Testing
bun run test # every unit test
bun test tests/unit/lib/dto # one folder
bun test --test-name-pattern settle # tests whose name matches
bun run test:integration # build, then HTTP flows against the server
bun run test:e2e # build, then Playwright in a browserThe suite runs on Bun's own test runner, with no Vitest and no Jest. It covers the domain logic (splits, balances, settle-up, recurrence, insights, receipt parsing, the category guess), the DTOs against a real SQLite database, the server modules (auth helpers, sign-up, setup, guards, origin check, secrets, mail, telemetry, migrations, the worker supervisor), the worker's job runner, the environment schemas, and components.
#The preload
Every test file runs after a preload that:
- compiles
.svelteand.svelte.tsfiles for the client, so components and rune modules import as they do in the browser; - mocks
$app/paths(resolveadds the leading slash and drops route groups),$app/envand$app/server; - calls
configure()with a testConfig: an in-memory SQLite database, a testAUTH_SECRET, telemetry off. A test that needs another value changes it throughgetConfig().
#Databases
Tests that touch data call resetDatabase(), which applies the real migrations
to the in-memory database and empties every table. Each test starts from a
known, empty state, and the migrations themselves are exercised on every run.
#Components
Component tests register happy-dom themselves, in beforeAll. It is not
registered globally on purpose: happy-dom replaces fetch, Request and
Headers with versions that strip the Origin header, which breaks every
server test that checks it.
#Rules the runner enforces
- Every test runs three times (
rerunEach = 3), so a flaky test fails locally instead of in CI a week later. - Coverage is checked per file: 80% of functions and lines, generated code, shadcn primitives and the schema aside.
mock.moduleis process-global and permanent. Every mock of one module must return the same shape, or tests start depending on their order.svelte-sonneris mocked for that reason too: itsruneddependency only exports asveltecondition Bun's runner doesn't set.
#Integration and end-to-end
Both run the compiled server, as it ships, on a fresh SQLite file under tmp/,
and read the setup code from its log like an admin would.
tests/integrationdrives the HTTP surface with Bun's runner: the setup gate, a wrong setup code, a form posted from another origin, the first account becoming admin, sign-in, a group, an expense with its merchant and its repeating copies, insights, group privacy, archiving and sign-out. It posts form actions the wayuse:enhancedoes and checks what landed by reading the database. Its ownbunfig.tomlturns off the unit suite's reruns and coverage, since these flows are stateful.tests/e2eis Playwright. Asetupproject creates the admin through the real setup screens and saves the session, adesktopproject then takes a group from creation through an expense, insights and the archive, and aphoneproject adds an expense through the drawers on a Pixel-sized screen and checks nothing overflows sideways.
#When it runs
The pre-push hook runs the suite with bun run check and the import-cycle
check, and CI runs the same hooks plus the integration and end-to-end suites in
a job of their own. The Stop hook for AI agents runs it before a change can be
handed back.
This guide lives in the project repo: edit it there, and this page follows within a day.