Frontend integration tests (client_v2)
Browser-driven, end-to-end tests for the Svelte client (client_v2), run
against Spoolman as it is deployed for real: the production Docker image
(with the client bundle baked in and served on the same origin as the API)
backed by a real PostgreSQL database.
This is the sibling of ../tests_frontend, which drives the
same deployment with SPOOLMAN_LEGACY_CLIENT=TRUE to cover the legacy React
client. Both complement the backend HTTP suite in
../tests_integration, which never touches the UI.
What is covered
tests/smoke.spec.ts— starting from the app root, clicks through every top-bar tab (dashboard, labels, settings, back to the library) and confirms each page renders inside the app shell with no browser-console errors. A second test confirms the library actually reaches the API rather than falling into its "couldn't reach the backend" state.tests/crud.spec.ts— the core happy path in one session. client_v2 folds what the legacy client spread over three create forms into a single "Add spools" modal, so one submit creates a manufacturer, a filament and a spool. The result is verified back in the library list (group header, spool row, remaining weight) and on the dashboard, which groups spools by location.tests/search.spec.ts— the library's find-things subsystem, all of it new in client_v2 and all of it backed by real API calls: the top-bar cross-entity search (a filament name, a manufacturer name and a location each land in their own section, and a query that matches nothing says so), opening a result into its inspector, grouping by location and by manufacturer, filtering by location and clearing the chip again, and changing the sort. Group/sort/filter live in the URL, so each is also checked to survive a reload.tests/labels.spec.ts— the label/QR subsystem. A design is created, named and saved, then found again after a reload (designs live in thelabel_designsserver setting, not in localStorage), and a label for a real spool is exported through the "Files" mode in both formats: a non-trivial PNG carrying the layout's resolution in itspHYschunk, and an AML document embedding the same raster — which is the only way to prove the QR encoding, template substitution, logo and mm→pixel maths all ran. The "Print" modes are deliberately not exercised: they open the browser's print dialog, which would stall the session.tests/settings.spec.ts— the settings page, where two persistence models meet with no visual clue which is which: currency, price rounding and base URL are server settings (checked by reloading), while the theme is per-browser and re-applied by the inline script inapp.htmlbefore first paint. The extra-fields manager is covered end to end — define a spool field, see the library's filter menu pick it up from the field metadata, then delete it again.tests/pwa.spec.ts— installability. The manifest and its icons are plain files inclient_v2/static/rather than something a build plugin generates, so the suite checks the deployed artifact: the manifest is served at the deploy root with a type browsers accept, describes a standalone app with a maskable icon, every icon it promises resolves, and its URLs are relative (which is what lets the same file work underSPOOLMAN_BASE_PATH).tests/legacy-sw.spec.ts— the upgrade path off the legacy client's service worker. Everyone who ever opened the old React client has a worker registered at the deploy root whose precache serves the old app shell for all navigations, so unless something answers its update check for/sw.jsthe upgrade is invisible to them. The test stands in a worker that behaves like the old one, proves it does keep serving the old shell, then swaps the real/sw.jsback in and asserts the new UI returns on its own with the registration and every cache gone. The fix it covers isclient_v2/static/sw.js.
The tests navigate purely through the UI — the top bar to reach each page, the
"Add spools" button to reach the create modal. Only the initial "open the app"
step uses a direct URL (and tests/pwa.spec.ts, which is about what the server
serves rather than about the app). The app's language is forced to English (see
tests/fixtures.ts) so the label and button matchers are stable regardless of
the runner's browser locale.
Each spec creates the data it needs through createSpoolViaModal in
tests/helpers.ts rather than leaning on what an earlier spec happened to
leave behind, so specs can be run individually and in any order. Anything with
instance-wide reach — a settings value, an extra field, a label design — is put
back afterwards.
Running locally
From the repository root:
uv run poe itest-frontend-v2
This builds both client bundles (the image bakes in client_v2/build and, for
the SPOOLMAN_LEGACY_CLIENT fallback, client/dist), builds the
donkie/spoolman:test image, brings up the stack with Docker Compose, waits for
it to become healthy, and runs Playwright. It tears the stack down afterwards.
Useful environment variables:
SPOOLMAN_CONTAINER_ENGINE=podman— run with rootless Podman instead of Docker.SPOOLMAN_HOST_PORT=9000— publish Spoolman on a different host port (default8001, one above the legacy suite's8000so both stacks can be up at the same time).
Running the tests against an already-running instance
If you already have Spoolman running somewhere, you can skip the orchestration and point Playwright straight at it:
cd tests_frontend_v2
npm ci
npx playwright install --with-deps chromium
SPOOLMAN_BASE_URL=http://localhost:8000 npx playwright test
The tests create data with unique, non-ASCII-tagged names, so they are safe to run repeatedly against a persistent instance (though a fresh database is what CI uses).
CI
The test-frontend-v2 job in .github/workflows/ci.yml
reuses the same donkie/spoolman:test image built for the backend integration
tests, so the frontend is verified against the exact artifact that ships.
On failure it uploads the Playwright HTML report as a build artifact.