Browser coverage was smoke plus CRUD, mirroring the old React suite -- which left the subsystems with the most new code in them completely untested. Everything added here needs a real deployment to mean anything, which is why it lives in this suite and not in vitest. search.spec.ts drives the top-bar search (a filament, a manufacturer and a location each land in their own section; spools match on their own text, not their filament's, so a location is what finds one) and the toolbar's group / filter / sort. Group, sort and filters live in the URL, so each is checked to survive a reload. Sorting a grouped list by something the backend cannot order groups by flattens the view, and that is asserted rather than left as a surprise. labels.spec.ts round-trips a design through the label_designs server setting and exports a label for a real spool through the "Image files" mode, asserting a non-trivial PNG comes back -- the only way to prove the QR encoding, template substitution, logo and mm-to-pixel maths actually ran. The print modes are left alone on purpose: they open the browser's print dialog, which would stall the session. settings.spec.ts separates the two persistence models the page mixes with no visual cue -- currency, rounding and base URL on the server, theme in the browser -- and takes an extra field from definition to the library's filter menu and back out again. pwa.spec.ts checks the deployed manifest rather than the source file: served at the deploy root, with a type browsers accept, all icons resolving, all URLs relative. The add-spool flow moves out of crud.spec.ts into a helper so each spec creates its own data instead of leaning on what an earlier one left behind, and filters pin assertions to that data -- the library is paginated, so an unfiltered assertion about a specific row is a coin toss on an instance with real data in it. Anything with instance-wide reach is put back afterwards. Verified against both an empty database and one holding ~50 seeded spools. Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
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 "Image files" mode and asserted to be a non-trivial PNG — 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.