Testing
Surplus is tested primarily with end-to-end tests that drive the real app in a browser against ephemeral infrastructure. The tests/ package also has unit tests for E2E helpers (bucket naming, Tigris utilities, and similar).
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 the parent branch, destroyed after the run.
- A fresh Upstash Redis instance, destroyed after the run.
- A Tigris file-storage fork — copy-on-write fork of the env’s
S3_FILE_STORAGE_BUCKET_NAMEinto an ephemeraltest-surplus-{env}-{run}bucket (16-char hex run suffix), force-deleted 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
tigris.ts — Tigris fork bucket 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 (test helpers)
The tests/ package uses Bun's test runner for helper logic — not the web client:
cd tests && bun run test:unitContinuous integration
Pull requests targeting production or staging run two GitHub Actions workflows (when the PR's source branch is on the team dev-branch allowlist):
Quality (.github/workflows/quality.yml)
A single quality job on ubuntu-latest — one bun install, then:
bunx biome check .— read-only lint and format checkbun run typecheck—tsgoacross every@surplus/*workspacebun run knip:ci— unused files and dependencies
No Railway secrets are required. Locally, bun run lint auto-fixes with Biome; CI uses the read-only check above.
E2E (.github/workflows/e2e.yml)
Playwright against live server and client dev instances. CI uses the Railway CLI to inject configuration at runtime, so the only GitHub secret required per branch is a scoped RAILWAY_TOKEN on the GitHub Environment surplus / {branch} — everything else (Neon, Upstash, JWT, Resend, S3, Stripe, Vite) is pulled live from Railway.
The workflow installs Bun and Playwright Chromium, then runs the orchestrator (tests/src/main.ts), which provisions an ephemeral Neon branch, Upstash Redis instance, and Tigris fork bucket, runs the suite, and tears it all down.
Docs-only changes under apps/docs/**, docs/**, and **/*.md skip the E2E workflow via path filters. The quality workflow has no path filters.
Both workflows also support manual workflow_dispatch from the Actions tab.