Surplus logo
Surplus Docsby Sharing Excess

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_test cookie 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.ts

Layout

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 tests

Writing 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 test

Continuous 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.