Testing
Surplus is tested at two levels: end-to-end tests that drive the real app in a browser, and unit tests for client logic. The end-to-end suite is the centerpiece, because it exercises the real server, real client, and a real (throwaway) database.
End-to-end tests (Playwright)
The tests/ package runs Playwright against live server and client dev instances backed by ephemeral infrastructure:
- A Neon database branch forked from staging, destroyed after the run.
- A fresh Upstash Redis instance, destroyed after the run.
- JWT cookie injection — most tests skip the sign-in flow by signing a JWT and setting the
se_surplus_auth_testcookie directly on the browser context.
The orchestrator (tests/src/main.ts) provisions the infrastructure, spawns Playwright, and tears everything down regardless of the outcome.
Running
# the whole suite (Railway injects the environment)
bun run test
# filtered by path
bun run test src/specs/admin/
bun run test src/specs/auth/otpValidation.spec.tsLayout
tests/src/
main.ts — orchestrator entry point
helpers/
env.ts — Zod-validated env
neon.ts — Neon branch create/delete
upstash.ts — Upstash Redis create/delete
auth.ts — JWT signing + per-test cookie injection
seed.ts — test data factories
specs/
public/ — unauthenticated pages
auth/ — sign-in / OTP / logout flows
admin/ — admin-role tests
partner/ — partner-role tests
driver/ — driver-role testsWriting tests
Authenticated tests (admin, partner, driver) import the auth helper as a side effect to seed a user and inject a session cookie:
import { expect, test } from '@playwright/test'
import '../../helpers/auth'
import { waitForAuth } from '../../helpers/auth'
test('admin sees home page', async ({ page }) => {
await page.goto('/')
await waitForAuth(page)
await expect(page.getByText('Welcome')).toBeVisible()
})Unauthenticated tests (public pages, the sign-in flow) deliberately do not import the auth helper, so they exercise the real flow. OTP flow tests read the one-time code straight from the test Redis instance to complete sign-in without email access.
Selectors
Prefer the stable element IDs the app already sets ({routeName}{sectionName}{elementName}) over text or CSS selectors. This is one of the practical payoffs of the element-ID convention.
Unit tests (client)
The web client uses Vitest for unit and component tests:
cd apps/client && bun run testContinuous integration
End-to-end tests run automatically in GitHub Actions (.github/workflows/e2e.yml) on pushes and pull requests targeting main and staging. CI uses the Railway CLI to inject every secret at runtime, so the only GitHub secret required is a scoped RAILWAY_TOKEN — everything else (Neon, Upstash, JWT, Resend, S3, Stripe, Vite) is pulled live from Railway. The workflow checks out the repo, installs with Bun, installs Playwright's Chromium, and runs the orchestrator, which provisions the ephemeral Neon branch and Upstash instance, runs the suite, and tears it all down.