diff --git a/.gitea/workflows/ui-release.yaml b/.gitea/workflows/ui-release.yaml new file mode 100644 index 0000000..e6f072f --- /dev/null +++ b/.gitea/workflows/ui-release.yaml @@ -0,0 +1,145 @@ +name: ui-release + +# Tier 2 (release) workflow. Runs on tag push. +# +# Currently mirrors the Tier 1 step set. Visual regression baseline +# checks and the macOS-runner iOS smoke job are landed in later phases +# of ui/PLAN.md and live as commented sections at the end of this file +# until those phases ship. + +on: + push: + tags: + - 'v*' + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.work + cache: true + + - name: Run Go tests + # client/ is the deprecated Fyne client; excluded from CI per + # ui/PLAN.md §74. -count=1 disables Go's test cache so a green + # run never depends on a previous runner's cached state. The + # backend suite is run with -p 1 because most backend packages + # spawn their own Postgres testcontainer, and parallel + # Postgres bootstraps starve each other on a constrained + # runner. pkg modules are listed one by one because ./pkg/... + # does not recurse across the independent go.work modules + # under pkg/. + run: | + 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/... + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 11.0.7 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: ui/pnpm-lock.yaml + + - name: Install npm dependencies + working-directory: ui + run: pnpm install --frozen-lockfile + + - name: Install Playwright browsers + working-directory: ui/frontend + run: pnpm exec playwright install --with-deps + + - name: Run Vitest + working-directory: ui/frontend + run: pnpm test + + - name: Run Playwright + working-directory: ui/frontend + run: pnpm exec playwright test + + - name: Upload Playwright report on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: ui/frontend/playwright-report/ + retention-days: 14 + + - name: Upload Playwright traces on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-traces + path: ui/frontend/test-results/ + retention-days: 14 + +# visual-regression: enabled in Phase 33 of ui/PLAN.md, once the PWA +# shell and service worker land and a snapshot baseline is committed +# under ui/frontend/tests/__snapshots__/. +# +# visual-regression: +# runs-on: ubuntu-latest +# needs: test +# steps: +# - uses: actions/checkout@v4 +# - uses: pnpm/action-setup@v4 +# with: { version: 11.0.7 } +# - uses: actions/setup-node@v4 +# with: +# node-version: 22 +# cache: pnpm +# cache-dependency-path: ui/pnpm-lock.yaml +# - working-directory: ui +# run: pnpm install --frozen-lockfile +# - working-directory: ui/frontend +# run: pnpm exec playwright install --with-deps +# - working-directory: ui/frontend +# run: pnpm exec playwright test --grep @visual + +# ios-smoke: enabled in Phase 32 of ui/PLAN.md, once the Capacitor +# wrapper lands. Runs a Capacitor + Appium smoke against an iOS +# simulator on a macOS runner. +# +# ios-smoke: +# runs-on: macos-13 +# needs: test +# steps: +# - uses: actions/checkout@v4 +# - uses: pnpm/action-setup@v4 +# with: { version: 11.0.7 } +# - uses: actions/setup-node@v4 +# with: +# node-version: 22 +# cache: pnpm +# cache-dependency-path: ui/pnpm-lock.yaml +# - working-directory: ui +# run: pnpm install --frozen-lockfile +# - working-directory: ui/mobile +# run: pnpm exec cap sync ios && pnpm exec appium-smoke ios diff --git a/.gitea/workflows/ui-test.yaml b/.gitea/workflows/ui-test.yaml new file mode 100644 index 0000000..5334a39 --- /dev/null +++ b/.gitea/workflows/ui-test.yaml @@ -0,0 +1,119 @@ +name: ui-test + +# Tier 1 (per-PR) workflow. Runs Vitest + Playwright for the UI client and +# the monorepo Go service tests (everything except the integration suite, +# which lives behind `make -C integration integration` and needs a Docker +# daemon set up for testcontainers). +# +# The path filter is intentionally broad until a dedicated go-test +# workflow is introduced; this is the only CI gate today. + +on: + push: + paths: + - 'ui/**' + - 'backend/**' + - 'gateway/**' + - 'game/**' + - 'pkg/**' + - 'go.work' + - 'go.work.sum' + - '.gitea/workflows/ui-test.yaml' + pull_request: + paths: + - 'ui/**' + - 'backend/**' + - 'gateway/**' + - 'game/**' + - 'pkg/**' + - 'go.work' + - 'go.work.sum' + - '.gitea/workflows/ui-test.yaml' + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.work + cache: true + + - name: Run Go tests + # client/ is the deprecated Fyne client; excluded from CI per + # ui/PLAN.md §74. -count=1 disables Go's test cache so a green + # run never depends on a previous runner's cached state. The + # backend suite is run with -p 1 because most backend packages + # spawn their own Postgres testcontainer, and parallel + # Postgres bootstraps starve each other on a constrained + # runner. pkg modules are listed one by one because ./pkg/... + # does not recurse across the independent go.work modules + # under pkg/. + run: | + 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/... + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 11.0.7 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: ui/pnpm-lock.yaml + + - name: Install npm dependencies + working-directory: ui + run: pnpm install --frozen-lockfile + + - name: Install Playwright browsers + working-directory: ui/frontend + run: pnpm exec playwright install --with-deps + + - name: Run Vitest + working-directory: ui/frontend + run: pnpm test + + - name: Run Playwright + working-directory: ui/frontend + run: pnpm exec playwright test + + - name: Upload Playwright report on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: ui/frontend/playwright-report/ + retention-days: 14 + + - name: Upload Playwright traces on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-traces + path: ui/frontend/test-results/ + retention-days: 14 diff --git a/tools/local-ci/.gitignore b/tools/local-ci/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/tools/local-ci/.gitignore @@ -0,0 +1 @@ +.env diff --git a/tools/local-ci/Makefile b/tools/local-ci/Makefile new file mode 100644 index 0000000..82be826 --- /dev/null +++ b/tools/local-ci/Makefile @@ -0,0 +1,42 @@ +.PHONY: help up down logs status clean push + +.DEFAULT_GOAL := help + +COMPOSE := docker compose +GITEA_USER := galaxy +GITEA_PASS := galaxy-dev +REPO_NAME := galaxy +REMOTE_NAME := local-gitea +REPO_ROOT := $(realpath $(CURDIR)/../..) +GIT := git -C $(REPO_ROOT) +REMOTE_URL := http://$(GITEA_USER):$(GITEA_PASS)@localhost:3000/$(GITEA_USER)/$(REPO_NAME).git + +help: + @echo "Local Gitea CI for galaxy:" + @echo " make up Bring up Gitea + runner (idempotent)" + @echo " make down Stop both containers" + @echo " make logs Tail logs" + @echo " make status Show container status" + @echo " make push Push current branch to local Gitea" + @echo " make clean Stop and wipe all local state" + +up: + @./bootstrap.sh + +down: + $(COMPOSE) down + +logs: + $(COMPOSE) logs -f --tail=50 + +status: + $(COMPOSE) ps + +push: + @$(GIT) remote get-url $(REMOTE_NAME) >/dev/null 2>&1 || \ + $(GIT) remote add $(REMOTE_NAME) $(REMOTE_URL) + $(GIT) push $(REMOTE_NAME) HEAD + +clean: + $(COMPOSE) down -v + rm -f .env diff --git a/tools/local-ci/README.md b/tools/local-ci/README.md new file mode 100644 index 0000000..93990a9 --- /dev/null +++ b/tools/local-ci/README.md @@ -0,0 +1,98 @@ +# Local Gitea CI + +Self-contained Gitea + Actions runner for verifying +`.gitea/workflows/*` honestly before pushing to a real Gitea instance. +Runs natively on arm64 (Apple Silicon) — every image below has an +arm64 variant, so Docker pulls the right architecture and the runner +executes workflow steps without QEMU emulation. + +## Prerequisites + +- Docker (Colima or Docker Desktop) +- `python3`, `curl`, `bash` — all built into macOS + +## First time + +```sh +make -C tools/local-ci up +``` + +This: + +1. brings up the Gitea container; +2. creates an admin user (`galaxy` / `galaxy-dev`); +3. creates the `galaxy/galaxy` repo; +4. fetches a runner registration token from the Gitea API; +5. brings up the runner with that token (the runner persists its + credentials in a Docker volume and ignores the token on subsequent + restarts). + +The script is idempotent — re-running it is safe. + +## Pushing a branch + +```sh +make -C tools/local-ci push +``` + +This adds a `local-gitea` remote on the first run and then pushes the +current `HEAD`. Equivalent manual flow: + +```sh +git remote add local-gitea \ + http://galaxy:galaxy-dev@localhost:3000/galaxy/galaxy.git +git push local-gitea HEAD +``` + +The Tier 1 workflow fires on `push` to any branch and the Tier 2 +workflow fires on tags matching `v*`. Watch runs at: + + + +## Operational targets + +| Target | What it does | +| ---------------- | -------------------------------------------- | +| `make up` | Bring up Gitea + runner (idempotent) | +| `make down` | Stop both containers (state preserved) | +| `make logs` | Tail logs from both containers | +| `make status` | Show container status | +| `make push` | Push current `HEAD` to local Gitea | +| `make clean` | Stop and wipe all local state (full reset) | + +## What's in the box + +| Component | Image | Role | +| ---------- | ---------------------------------- | ------------------------------------------- | +| Gitea | `gitea/gitea:1.23` | Server with SQLite backend | +| act_runner | `gitea/act_runner:0.6.1` | Single-capacity runner registered on boot | +| Workflow | `catthehacker/ubuntu:act-latest` | Image spawned per job (multi-arch) | + +The runner mounts the host Docker socket and spawns workflow +containers on the same Docker network as Gitea, so +`actions/checkout` reaches the server at `http://gitea:3000` from +inside spawned containers. + +## Caveats + +- Gitea's `ROOT_URL` is set to `http://gitea:3000/` so spawned + workflow containers reach the server through the compose network. + The web UI works at `http://localhost:3000` via port mapping, but + copy-paste URLs in the UI may show `gitea:3000` instead of + `localhost:3000`. Harmless for local dev; switch the host part by + hand when copying. +- The runner is single-capacity (`runner.capacity: 1` in + `config.yaml`). Concurrent jobs queue. Bump if you need parallel + jobs. +- First push from a fresh checkout uploads the full repo history + (~tens of MB). Subsequent pushes are deltas. +- `actions/upload-artifact@v4` requires Gitea ≥ 1.21 — we pin + `1.23` to stay above the cutoff. +- Workflow steps run as `root` inside the spawned container; this + matches the upstream catthehacker behaviour. Keep that in mind if + you add steps that touch host-mounted directories. +- On Apple Silicon the runner image and its catthehacker child run + natively as arm64. Some pre-built tools that ship in the image are + amd64-only and would fall back to QEMU; `setup-go`, `setup-node`, + and `pnpm/action-setup` all download arm64 binaries themselves, so + the workflow steps we care about stay native. diff --git a/tools/local-ci/bootstrap.sh b/tools/local-ci/bootstrap.sh new file mode 100755 index 0000000..7e81dc1 --- /dev/null +++ b/tools/local-ci/bootstrap.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Bring up Gitea, create the admin user and the galaxy/galaxy repo, +# fetch a runner registration token, bring up the runner. +# Idempotent — re-runnable. +set -euo pipefail + +cd "$(dirname "$0")" + +GITEA_USER=galaxy +GITEA_PASS=galaxy-dev +GITEA_EMAIL=galaxy@local +REPO_NAME=galaxy +GITEA_URL=http://localhost:3000 + +echo ">>> Bringing up Gitea..." +docker compose up -d gitea + +echo ">>> Waiting for Gitea API..." +for _ in $(seq 1 120); do + if curl -fsS "${GITEA_URL}/api/v1/version" >/dev/null 2>&1; then + echo "Gitea is up." + break + fi + sleep 1 +done + +if ! curl -fsS "${GITEA_URL}/api/v1/version" >/dev/null 2>&1; then + echo "Gitea did not come up within 120 seconds." >&2 + docker compose logs gitea | tail -30 >&2 + exit 1 +fi + +echo ">>> Creating admin user (idempotent)..." +docker compose exec -T gitea su git -c " + gitea admin user create \ + --username ${GITEA_USER} \ + --password ${GITEA_PASS} \ + --email ${GITEA_EMAIL} \ + --admin \ + --must-change-password=false 2>&1 || true +" + +echo ">>> Creating repo (idempotent)..." +HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' \ + -u "${GITEA_USER}:${GITEA_PASS}" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"${REPO_NAME}\",\"private\":true,\"auto_init\":false}" \ + "${GITEA_URL}/api/v1/user/repos") +case "${HTTP_CODE}" in + 201) echo "Repo created." ;; + 409) echo "Repo already exists." ;; + *) + echo "Unexpected response (${HTTP_CODE}) creating repo." >&2 + exit 1 + ;; +esac + +echo ">>> Fetching runner registration token..." +RUNNER_TOKEN=$(curl -fsS \ + -u "${GITEA_USER}:${GITEA_PASS}" \ + "${GITEA_URL}/api/v1/admin/runners/registration-token" \ + | python3 -c "import json, sys; print(json.load(sys.stdin)['token'])") + +# act_runner uses RUNNER_TOKEN only on the first boot. After registration +# it persists credentials in the named runner-data volume (/data/.runner) +# and ignores the env token on subsequent restarts. Writing a fresh token +# every time is harmless. +echo "RUNNER_TOKEN=${RUNNER_TOKEN}" > .env + +echo ">>> Bringing up runner..." +docker compose up -d runner + +cat </dev/null || true + git push local-gitea HEAD + + open http://localhost:3000/${GITEA_USER}/${REPO_NAME}/actions + +Or use \`make push\` from this directory. +EOF diff --git a/tools/local-ci/config.yaml b/tools/local-ci/config.yaml new file mode 100644 index 0000000..8f34468 --- /dev/null +++ b/tools/local-ci/config.yaml @@ -0,0 +1,35 @@ +# act_runner configuration. +# +# The `ubuntu-latest` label is mapped to catthehacker/ubuntu:act-latest, +# which is multi-arch — Docker on Apple Silicon pulls the arm64 variant +# and runs it natively (no QEMU). The same image is what `act` uses +# locally, so workflows behave the same. + +log: + level: info + +runner: + file: /data/.runner + capacity: 1 + fetch_timeout: 5s + fetch_interval: 2s + labels: + - "ubuntu-latest:docker://catthehacker/ubuntu:act-latest" + +cache: + enabled: true + dir: /data/cache + +container: + # Spawned workflow containers join the same network as Gitea so + # actions/checkout and other steps can reach the server at + # http://gitea:3000. + network: galaxy-local-gitea-net + privileged: false + options: "" + workdir_parent: "" + valid_volumes: [] + force_pull: false + +host: + workdir_parent: "" diff --git a/tools/local-ci/docker-compose.yml b/tools/local-ci/docker-compose.yml new file mode 100644 index 0000000..2586dcb --- /dev/null +++ b/tools/local-ci/docker-compose.yml @@ -0,0 +1,78 @@ +# Local Gitea + Actions runner for verifying .gitea/workflows/*. +# Runs natively on arm64 (Apple Silicon) — every image below is multi-arch. +# +# Browser: http://localhost:3000 +# API: http://localhost:3000/api/v1 +# Push URL: http://galaxy:galaxy-dev@localhost:3000/galaxy/galaxy.git +# Actions: http://localhost:3000/galaxy/galaxy/actions +# +# `bootstrap.sh` (or `make up`) brings everything up and registers the +# runner. State persists in named Docker volumes; `make clean` wipes them. + +services: + gitea: + image: gitea/gitea:1.23 + container_name: galaxy-local-gitea + restart: unless-stopped + environment: + USER_UID: "1000" + USER_GID: "1000" + GITEA__database__DB_TYPE: sqlite3 + GITEA__database__PATH: /data/gitea/gitea.db + # ROOT_URL uses the in-network hostname so the runner and spawned + # workflow containers reach Gitea through the compose network. + # The browser still works at http://localhost:3000 via the port + # mapping below; UI-generated copy URLs may show "gitea:3000", + # which is harmless for local dev. + GITEA__server__ROOT_URL: http://gitea:3000/ + GITEA__server__SSH_PORT: "2222" + GITEA__actions__ENABLED: "true" + GITEA__security__INSTALL_LOCK: "true" + GITEA__service__DISABLE_REGISTRATION: "true" + ports: + - "3000:3000" + - "2222:22" + volumes: + - gitea-data:/data + networks: + - gitea-net + healthcheck: + test: + - CMD-SHELL + - wget -q -O- http://localhost:3000/api/v1/version >/dev/null || exit 1 + interval: 5s + timeout: 3s + retries: 30 + start_period: 5s + + runner: + image: gitea/act_runner:0.6.1 + container_name: galaxy-local-runner + restart: unless-stopped + depends_on: + gitea: + condition: service_healthy + environment: + CONFIG_FILE: /config/config.yaml + GITEA_INSTANCE_URL: http://gitea:3000 + # Provided by bootstrap.sh in the .env file. After the first + # successful registration, act_runner persists credentials in + # /data/.runner and ignores this token on subsequent restarts. + GITEA_RUNNER_REGISTRATION_TOKEN: ${RUNNER_TOKEN:-} + GITEA_RUNNER_NAME: galaxy-local + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - runner-data:/data + - ./config.yaml:/config/config.yaml:ro + networks: + - gitea-net + +networks: + gitea-net: + name: galaxy-local-gitea-net + +volumes: + gitea-data: + name: galaxy-local-gitea-data + runner-data: + name: galaxy-local-runner-data diff --git a/ui/PLAN.md b/ui/PLAN.md index 00d4237..f17df48 100644 --- a/ui/PLAN.md +++ b/ui/PLAN.md @@ -287,9 +287,9 @@ Targeted tests: - a single Vitest smoke test that mounts the landing component and asserts the rendered version string is non-empty. -## Phase 2. Testing Infrastructure +## ~~Phase 2. Testing Infrastructure~~ -Status: pending. +Status: done. Goal: install and configure the test toolchain that every later phase depends on, including Tier 1 (per-PR) and Tier 2 (release) targets. @@ -299,17 +299,37 @@ Artifacts: - `ui/frontend/package.json` dev-dependencies (added on top of the Phase 1 minimum of `vitest`, `jsdom`, `@testing-library/svelte`): `@testing-library/jest-dom`, `playwright`, `@playwright/test` -- `ui/frontend/vitest.config.ts` extended for `@testing-library/jest-dom` - matchers (the JSDOM environment itself is wired in Phase 1) -- `ui/frontend/playwright.config.ts` with three projects: +- `ui/frontend/vitest.config.ts` extended with `setupFiles: + ["./tests/setup.ts"]` to wire `@testing-library/jest-dom` matchers + into Vitest (the JSDOM environment itself is wired in Phase 1) +- `ui/frontend/tests/setup.ts` registering `jest-dom` matchers +- `ui/frontend/tests/e2e/landing.spec.ts` placeholder Playwright test + asserting the version footer renders +- `ui/frontend/playwright.config.ts` with four projects: `chromium-desktop`, `webkit-desktop`, `chromium-mobile-iphone-13`, - `chromium-mobile-pixel-5`; tracing and screenshots enabled on failure -- `ui/.gitea/workflows/test.yaml` running Tier 1 on every push and PR - on a Linux runner: `go test ./...`, `pnpm test`, `pnpm exec - playwright install --with-deps`, `pnpm exec playwright test` -- `ui/.gitea/workflows/release.yaml` running Tier 2 on tag push: - visual regression baseline check, optional macOS runner block for - iOS smoke (Phase 32+ only) + `chromium-mobile-pixel-5`; tracing and screenshots enabled on + failure; `webServer: pnpm run dev` on port 5173 +- `.gitea/workflows/ui-test.yaml` running Tier 1 on every push and PR + on a Linux runner: monorepo Go service tests for `backend/`, + `gateway/`, `game/`, and every `pkg//` module (each pkg + module is enumerated explicitly because they sit as independent + go.work modules under a shared `pkg/` directory, and `./pkg/...` + does not recurse across module boundaries). All Go tests run with + `-count=1` so the cache never masks a failing run; backend tests + additionally run with `-p 1` because most backend packages spawn + their own Postgres testcontainer and parallel bootstraps starve + each other on the runner. The integration suite stays gated behind + `make -C integration integration` and lives outside Tier 1; the + deprecated `client/` Fyne client (see §74) is also excluded — its + tests, code, and documentation are frozen and CI must not run + them. Then `pnpm install --frozen-lockfile` from `ui/`, + `pnpm exec playwright install --with-deps`, `pnpm test`, + `pnpm exec playwright test`; Playwright reports and traces + uploaded as artefacts on failure +- `.gitea/workflows/ui-release.yaml` running Tier 2 on tag push (`v*`): + same Tier 1 step set today; visual-regression and macOS-runner + iOS-smoke jobs live as commented sections marked with the phase + number that re-enables them (Phase 33 and Phase 32 respectively) - `ui/docs/testing.md` topic doc naming the two tiers, the tools per tier, and the rule that visual regression baselines live in `ui/frontend/tests/__snapshots__/` until shifted to Argos @@ -322,7 +342,9 @@ Acceptance criteria: - a placeholder Playwright test passes in `chromium-desktop` and `webkit-desktop` projects locally; - the Gitea Actions Tier 1 workflow runs end-to-end against a clean - clone of the repo on a Linux runner. + clone of the repo on a Linux runner. Until the Gitea runner is + provisioned, the workflow is exercised locally with + `act -W .gitea/workflows/ui-test.yaml`. Targeted tests: @@ -342,6 +364,9 @@ implementation. No network, no UI. Artifacts: - `ui/core/go.mod` module declared in the project Go workspace +- `.gitea/workflows/ui-test.yaml` and `.gitea/workflows/ui-release.yaml` + extended to add `./ui/core/...` to the Tier 1 / Tier 2 `go test` + command list introduced in Phase 2 - `ui/core/canon/` canonical bytes for `galaxy-request-v1`, `galaxy-response-v1`, and `galaxy-event-v1`, matching `docs/ARCHITECTURE.md` §15 byte-for-byte @@ -1702,7 +1727,7 @@ Artifacts: follow-up issue to switch to self-hosted Argos - Appium harness for iOS Simulator and Android Emulator covering the login flow, push-event flow, and at least one full turn loop; - `.gitea/workflows/release.yaml` extended with macOS-runner Appium + `.gitea/workflows/ui-release.yaml` extended with macOS-runner Appium job (mandatory pre-release gate) Dependencies: Phases 1 through 35. diff --git a/ui/docs/testing.md b/ui/docs/testing.md new file mode 100644 index 0000000..5e83635 --- /dev/null +++ b/ui/docs/testing.md @@ -0,0 +1,92 @@ +# 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/**`, `client/**`, `go.work`, or `go.work.sum`. Linux runner only. + +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/... +``` + +## CI dry-run with `act` + +Until the Gitea Actions runner is wired up, the workflow is exercised +locally with [`act`](https://github.com/nektos/act): + +```sh +act -W .gitea/workflows/ui-test.yaml --container-architecture linux/amd64 +``` + +`act` reads the workflow as GitHub Actions; the format is +intentionally compatible. diff --git a/ui/frontend/package.json b/ui/frontend/package.json index 93de661..7d23655 100644 --- a/ui/frontend/package.json +++ b/ui/frontend/package.json @@ -8,15 +8,19 @@ "build": "vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "test": "vitest run" + "test": "vitest run", + "test:e2e": "playwright test" }, "devDependencies": { + "@playwright/test": "^1.59.1", "@sveltejs/adapter-static": "^3.0.0", "@sveltejs/kit": "^2.59.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@testing-library/jest-dom": "^6.9.1", "@testing-library/svelte": "^5.2.0", "@types/node": "^22.0.0", "jsdom": "^25.0.0", + "playwright": "^1.59.1", "svelte": "^5.0.0", "svelte-check": "^4.0.0", "tslib": "^2.6.0", diff --git a/ui/frontend/playwright.config.ts b/ui/frontend/playwright.config.ts new file mode 100644 index 0000000..b157aa0 --- /dev/null +++ b/ui/frontend/playwright.config.ts @@ -0,0 +1,33 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "tests/e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + reporter: [["list"], ["html", { open: "never" }]], + use: { + baseURL: "http://localhost:5173", + trace: "retain-on-failure", + screenshot: "only-on-failure", + }, + projects: [ + { name: "chromium-desktop", use: { ...devices["Desktop Chrome"] } }, + { name: "webkit-desktop", use: { ...devices["Desktop Safari"] } }, + // devices["iPhone 13"] picks WebKit by default; the project name + // here claims a Chromium engine on a mobile viewport, so the + // browser is explicitly overridden. WebKit on a desktop viewport + // is already covered by webkit-desktop. + { + name: "chromium-mobile-iphone-13", + use: { ...devices["iPhone 13"], browserName: "chromium" }, + }, + { name: "chromium-mobile-pixel-5", use: { ...devices["Pixel 5"] } }, + ], + webServer: { + command: "pnpm run dev", + url: "http://localhost:5173", + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, +}); diff --git a/ui/frontend/tests/e2e/landing.spec.ts b/ui/frontend/tests/e2e/landing.spec.ts new file mode 100644 index 0000000..dece0ed --- /dev/null +++ b/ui/frontend/tests/e2e/landing.spec.ts @@ -0,0 +1,8 @@ +import { expect, test } from "@playwright/test"; + +test("landing page renders the version string", async ({ page }) => { + await page.goto("/"); + const footer = page.getByTestId("app-version"); + await expect(footer).toBeVisible(); + await expect(footer).toContainText(/version\s+\S+/); +}); diff --git a/ui/frontend/tests/landing.test.ts b/ui/frontend/tests/landing.test.ts index 61185c5..2e7ffe3 100644 --- a/ui/frontend/tests/landing.test.ts +++ b/ui/frontend/tests/landing.test.ts @@ -6,6 +6,7 @@ describe("landing page", () => { it("renders a non-empty version string in the footer", () => { const { getByTestId } = render(Page); const footer = getByTestId("app-version"); + expect(footer).toBeInTheDocument(); expect(footer.textContent?.trim()).not.toBe(""); expect(footer.textContent).toMatch(/version\s+\S+/); }); diff --git a/ui/frontend/tests/setup.ts b/ui/frontend/tests/setup.ts new file mode 100644 index 0000000..f149f27 --- /dev/null +++ b/ui/frontend/tests/setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/ui/frontend/vitest.config.ts b/ui/frontend/vitest.config.ts index a211595..f53490d 100644 --- a/ui/frontend/vitest.config.ts +++ b/ui/frontend/vitest.config.ts @@ -12,6 +12,7 @@ export default mergeConfig( environment: "jsdom", include: ["tests/**/*.test.ts"], globals: true, + setupFiles: ["./tests/setup.ts"], }, }), ); diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index be9ba02..d74bb6e 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: frontend: devDependencies: + '@playwright/test': + specifier: ^1.59.1 + version: 1.59.1 '@sveltejs/adapter-static': specifier: ^3.0.0 version: 3.0.10(@sveltejs/kit@2.59.1(@sveltejs/vite-plugin-svelte@7.1.1(svelte@5.55.5)(vite@8.0.10(@types/node@22.19.17)))(svelte@5.55.5)(typescript@5.9.3)(vite@8.0.10(@types/node@22.19.17))) @@ -17,6 +20,9 @@ importers: '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 version: 7.1.1(svelte@5.55.5)(vite@8.0.10(@types/node@22.19.17)) + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 '@testing-library/svelte': specifier: ^5.2.0 version: 5.3.1(svelte@5.55.5)(vite@8.0.10(@types/node@22.19.17))(vitest@4.1.5(@types/node@22.19.17)(jsdom@25.0.1)(vite@8.0.10(@types/node@22.19.17))) @@ -26,6 +32,9 @@ importers: jsdom: specifier: ^25.0.0 version: 25.0.1 + playwright: + specifier: ^1.59.1 + version: 1.59.1 svelte: specifier: ^5.0.0 version: 5.55.5 @@ -47,6 +56,9 @@ importers: packages: + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -124,6 +136,11 @@ packages: '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} + engines: {node: '>=18'} + hasBin: true + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -265,6 +282,10 @@ packages: resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/svelte-core@1.0.0': resolution: {integrity: sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ==} engines: {node: '>=16'} @@ -399,6 +420,9 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssstyle@4.6.0: resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} @@ -441,6 +465,9 @@ packages: dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -499,6 +526,11 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -547,6 +579,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -668,6 +704,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -703,6 +743,16 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + engines: {node: '>=18'} + hasBin: true + postcss@8.5.14: resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} @@ -722,6 +772,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + rolldown@1.0.0-rc.17: resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -764,6 +818,10 @@ packages: std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + svelte-check@4.4.8: resolution: {integrity: sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w==} engines: {node: '>= 18.0.0'} @@ -966,6 +1024,8 @@ packages: snapshots: + '@adobe/css-tools@4.4.4': {} + '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -1048,6 +1108,10 @@ snapshots: '@oxc-project/types@0.127.0': {} + '@playwright/test@1.59.1': + dependencies: + playwright: 1.59.1 + '@polka/url@1.0.0-next.29': {} '@rolldown/binding-android-arm64@1.0.0-rc.17': @@ -1151,6 +1215,15 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.1 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + '@testing-library/svelte-core@1.0.0(svelte@5.55.5)': dependencies: svelte: 5.55.5 @@ -1270,6 +1343,8 @@ snapshots: cookie@0.6.0: {} + css.escape@1.5.1: {} + cssstyle@4.6.0: dependencies: '@asamuzakjp/css-color': 3.2.0 @@ -1298,6 +1373,8 @@ snapshots: dom-accessibility-api@0.5.16: {} + dom-accessibility-api@0.6.3: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -1347,6 +1424,9 @@ snapshots: hasown: 2.0.3 mime-types: 2.1.35 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -1404,6 +1484,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + indent-string@4.0.0: {} + is-potential-custom-element-name@1.0.1: {} is-reference@3.0.3: @@ -1509,6 +1591,8 @@ snapshots: dependencies: mime-db: 1.52.0 + min-indent@1.0.1: {} + mri@1.2.0: {} mrmime@2.0.1: {} @@ -1531,6 +1615,14 @@ snapshots: picomatch@4.0.4: {} + playwright-core@1.59.1: {} + + playwright@1.59.1: + dependencies: + playwright-core: 1.59.1 + optionalDependencies: + fsevents: 2.3.2 + postcss@8.5.14: dependencies: nanoid: 3.3.12 @@ -1549,6 +1641,11 @@ snapshots: readdirp@4.1.2: {} + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + rolldown@1.0.0-rc.17: dependencies: '@oxc-project/types': 0.127.0 @@ -1600,6 +1697,10 @@ snapshots: std-env@4.1.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + svelte-check@4.4.8(picomatch@4.0.4)(svelte@5.55.5)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31