mirror of
https://github.com/Donkie/Spoolman.git
synced 2026-10-22 01:52:17 -05:00
Mirrors tests_frontend, but drives the Svelte client instead of the legacy React one: same production image, same PostgreSQL-backed compose stack, same "only the first navigation uses a URL" discipline. - smoke.spec.ts clicks through every top-bar tab, asserting each route's title and the app shell, with no browser console errors; a second test proves the library reached the API rather than falling into its offline state. - crud.spec.ts walks the first-use happy path. client_v2 folds the legacy client's three create forms into one "Add spools" modal, so a manufacturer, a filament and a spool are created by a single submit, then verified back in the library list and on the locations page. Two client_v2-specific helpers earn their keep: the top bar renders its nav twice (desktop + mobile, one hidden by CSS) so nav locators need narrowing to the visible one, and NumberInput's accessible name picks up its stepper and help buttons, so numeric fields are found via their form item instead of getByLabel. The suite publishes on host port 8001 so it can run alongside the legacy one. CI gains a test-frontend-v2 job cloned from test-frontend, gating publish-images alongside it. Also fixes tests_frontend/run.py: it only built client/dist, but the Dockerfile copies client_v2/build unconditionally, so the image could not be built from a clean checkout.
141 lines
5.2 KiB
Python
141 lines
5.2 KiB
Python
"""Build and run the Playwright frontend integration tests for client_v2.
|
|
|
|
Spins up Spoolman from the production image (clients baked in) backed by a real
|
|
PostgreSQL database, waits for it to become healthy, then drives the Svelte
|
|
frontend through a browser with Playwright. Mirrors tests_frontend/run.py — the
|
|
same suite pointed at the legacy React client — including the
|
|
SPOOLMAN_CONTAINER_ENGINE switch for running under rootless Podman.
|
|
"""
|
|
|
|
# ruff: noqa: T201, INP001
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
# Container engine to use. Defaults to "docker"; set SPOOLMAN_CONTAINER_ENGINE=podman
|
|
# to run the suite with rootless Podman (both expose a compatible build/compose CLI).
|
|
ENGINE = os.environ.get("SPOOLMAN_CONTAINER_ENGINE", "docker")
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
FRONTEND_DIR = REPO_ROOT / "tests_frontend_v2"
|
|
COMPOSE_FILE = FRONTEND_DIR / "docker-compose.yml"
|
|
|
|
# One above the legacy suite's default so both stacks can be up at once.
|
|
HOST_PORT = os.environ.get("SPOOLMAN_HOST_PORT", "8001")
|
|
BASE_URL = f"http://localhost:{HOST_PORT}"
|
|
HEALTH_URL = f"{BASE_URL}/api/v1/health"
|
|
HEALTH_TIMEOUT = 120
|
|
|
|
|
|
def run(cmd: list[str], cwd: Path | None = None, env: dict[str, str] | None = None) -> int:
|
|
"""Run a command, echoing it, and return its exit code."""
|
|
print(f"$ {' '.join(cmd)}", flush=True)
|
|
return subprocess.call(cmd, cwd=cwd, env=env)
|
|
|
|
|
|
def die(message: str) -> None:
|
|
print(message, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def build_legacy_client() -> None:
|
|
"""Build the legacy React bundle the image also bakes in.
|
|
|
|
Not the client under test here, but the Dockerfile copies ``client/dist``
|
|
unconditionally (it is what SPOOLMAN_LEGACY_CLIENT falls back to), so the
|
|
image cannot be built without it.
|
|
"""
|
|
client_dir = REPO_ROOT / "client"
|
|
print("Building the legacy client bundle (VITE_APIURL=/api/v1)...")
|
|
# A stray .env would override .env.production, so remove it like CI does.
|
|
(client_dir / ".env").unlink(missing_ok=True)
|
|
(client_dir / ".env.production").write_text("VITE_APIURL=/api/v1\n")
|
|
if not (client_dir / "node_modules").exists() and run(["npm", "ci"], cwd=client_dir) != 0:
|
|
die("Failed to install client dependencies!")
|
|
if run(["npm", "run", "build"], cwd=client_dir) != 0:
|
|
die("Failed to build the client!")
|
|
|
|
|
|
def build_client_v2() -> None:
|
|
"""Build the Svelte client under test.
|
|
|
|
Mirrors the CI ``build-client`` job: no VITE_APIURL is set, so the client
|
|
resolves the API relative to its own base path — i.e. same-origin, which is
|
|
how it is actually deployed.
|
|
"""
|
|
client_dir = REPO_ROOT / "client_v2"
|
|
print("Building the client_v2 bundle...")
|
|
if not (client_dir / "node_modules").exists() and run(["npm", "ci"], cwd=client_dir) != 0:
|
|
die("Failed to install client_v2 dependencies!")
|
|
if run(["npm", "run", "build"], cwd=client_dir) != 0:
|
|
die("Failed to build client_v2!")
|
|
|
|
|
|
def wait_for_health() -> None:
|
|
print(f"Waiting for Spoolman to become healthy at {HEALTH_URL} ...")
|
|
deadline = time.monotonic() + HEALTH_TIMEOUT
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
with urllib.request.urlopen(HEALTH_URL, timeout=5) as resp: # noqa: S310
|
|
if resp.status == 200:
|
|
print("Spoolman is up.")
|
|
return
|
|
except Exception: # noqa: BLE001, S110 - server not ready yet, keep polling
|
|
pass
|
|
time.sleep(2)
|
|
die(f"Spoolman did not become healthy within {HEALTH_TIMEOUT}s.")
|
|
|
|
|
|
def compose(*args: str) -> int:
|
|
return run([ENGINE, "compose", "-f", str(COMPOSE_FILE), *args])
|
|
|
|
|
|
def main() -> None:
|
|
print(f"Building and running client_v2 frontend integration tests (engine: {ENGINE})...")
|
|
|
|
build_legacy_client()
|
|
build_client_v2()
|
|
|
|
print("Building Spoolman image...")
|
|
if run([ENGINE, "build", "-t", "donkie/spoolman:test", "."], cwd=REPO_ROOT) != 0:
|
|
die("Failed to build Spoolman!")
|
|
|
|
print("Installing Playwright and its browser...")
|
|
if run(["npm", "ci"], cwd=FRONTEND_DIR) != 0:
|
|
die("Failed to install Playwright dependencies!")
|
|
# No --with-deps here: it shells out to the system package manager (sudo/apt)
|
|
# and only works on Debian-based hosts. Dev machines that can run a browser
|
|
# already have the libraries; CI installs the OS deps in its own step.
|
|
if run(["npx", "playwright", "install", "chromium"], cwd=FRONTEND_DIR) != 0:
|
|
die("Failed to install the Playwright browser!")
|
|
|
|
# Clean slate, then bring the stack up.
|
|
compose("down", "-v")
|
|
if compose("up", "-d") != 0:
|
|
compose("logs")
|
|
compose("down", "-v")
|
|
die("Failed to start the Spoolman stack!")
|
|
|
|
exit_code = 1
|
|
try:
|
|
wait_for_health()
|
|
env = {**os.environ, "SPOOLMAN_BASE_URL": BASE_URL}
|
|
exit_code = run(["npx", "playwright", "test"], cwd=FRONTEND_DIR, env=env)
|
|
finally:
|
|
if exit_code != 0:
|
|
print("Tests failed; dumping Spoolman logs:")
|
|
compose("logs", "spoolman")
|
|
compose("down", "-v")
|
|
|
|
if exit_code != 0:
|
|
die("client_v2 frontend integration tests failed!")
|
|
print("client_v2 frontend integration tests passed!")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|