Surplus logo
Surplus Docsby Sharing Excess

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_NAME into an ephemeral test-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_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
    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 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 (test helpers)

The tests/ package uses Bun's test runner for helper logic — not the web client:

cd tests && bun run test:unit

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

  1. bunx biome check . — read-only lint and format check
  2. bun run typechecktsgo across every @surplus/* workspace
  3. bun 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.