Testing
The suite is end-to-end, not unit — it boots the full app against an
in-memory SQLite database and drives it through app.request() (no port,
no server process) across two files:
tests/app.test.ts— auth, guards, roles, password reset, Inertia protocol (409/404/SSR), CSRF, health, static assetstests/tus.test.ts— the tus protocol (creation, resume, checksum, termination, ownership) and the profile/avatar flows
Why --isolate is mandatory
Section titled “Why --isolate is mandatory”Each file is written as an independent suite: it sets its env
(DATABASE_PATH=:memory:, UPLOAD_DIR, RATE_LIMIT_AUTH_MAX=1000,
RATE_LIMIT_GLOBAL_MAX=10000, …) in beforeAll before
importing the app modules (config/db read env at import), and calls
db.close() in afterAll. Bun 1.3 runs all test files in one shared
process, so without --isolate one file’s teardown finalizes the next
file’s prepared statements (“Statement has finalized”) and cached config
leaks across files.
bun run test # = bun test --isolateThe helpers
Section titled “The helpers”const xhr = { "x-inertia": "true" }; // simulate an Inertia SPA request
async function call(path, options) { // app.request with cookie/body return app.request(`${BASE}${path}`, { method, headers, body });}
function sessionCookie(res) { // extract the session cookie const cookie = allSetCookies(res).find((c) => c.startsWith("session=")); return cookie ? cookie.split(";")[0]! : "";}
async function registerUser(email, password = "password123") { const res = await call("/register", { method: "POST", headers: xhr, body: { name: "Test User", email, password }, }); expect(res.status).toBe(303); return sessionCookie(res); // returns the cookie for later calls}The test pattern
Section titled “The test pattern”- Register a user (returns a session cookie).
- Act —
call()the endpoint withxhr+ cookie. - Assert — status, Inertia payload (
component,props.errors,props.auth.user), redirect location, headers.
Inertia XHR requests return JSON page payloads, so assertions are structured:
const data = await res.json();expect(data.component).toBe("Dashboard");expect(data.props.auth.user.email).toBe("loginok@example.com");expect(JSON.stringify(data.props)).not.toContain("passwordHash");Writing a new test
Section titled “Writing a new test”Mirror the helpers at the top of the file and add a case to the relevant
describe block — or a new block. Env is already configured by
beforeAll; if your test needs different env (e.g. tus expiration), set it
in a new test FILE (each --isolate process gets fresh globals).
Keep tests deterministic: no sleeps, no external network (fetch is mocked
in the Google OAuth block), and control time explicitly where it matters
(the expiration sweep takes now as a parameter for this reason).
Browser testing
Section titled “Browser testing”The E2E suite covers server behavior (status codes, redirects, Inertia payloads, CSRF, auth). It does not catch client-side errors — a page can return 200 with a valid payload and still crash in the browser because of a prop shape mismatch, a missing import, or a Svelte/React runtime error.
When testing UI changes manually, the browser console is the source of truth:
- Open DevTools → Console before interacting with the page.
- Reload and watch for red errors (runtime exceptions, failed imports, hydration mismatches).
- Check the Network tab for failed asset loads (404 on JS/CSS, wrong manifest paths).
- A blank page with no console error usually means the Inertia component
name doesn’t match a registered page — check
src/client/pages.ts.
bun run typecheck and bun run build catch type and compile errors, but
neither runs the client code. Only the browser does.