Testing — every layer, how to run it, and what each one proves
The platform ships with four test layers. Each one catches a different class of bug; none of them replaces the others.
| Layer | Where | Runner | What it proves |
|---|---|---|---|
| Backend unit/service tests | backend/ |
Jest | Business logic against a real Postgres + Redis (fares, ledger, dispatch, RBAC, …) |
| Admin component tests | admin/ |
Vitest | React components and helpers in isolation (jsdom) |
| Mobile unit tests | mobile/rider, mobile/driver |
Jest | Stores, i18n catalogs, screen logic with mocked native modules |
| End-to-end (this doc) | admin/e2e, mobile/*/e2e |
Playwright / Maestro | A real browser (or app) driving the real stack: UI → API → database and back |
Run the unit layers per surface:
cd backend && npm test # needs Postgres + Redis (see backend README)
cd admin && npm test
cd mobile/rider && npm test
cd mobile/driver && npm test
The rest of this document is about the end-to-end layer.
1. Playwright — the admin panel, end to end
admin/e2e/*.spec.ts drives a real Chromium through the real stack. Nothing
is mocked: the login uses the real OTP endpoints, every table is asserted
against what the API returns at that moment, and the one write flow round-trips
through Postgres.
Chromium ──► Next.js admin (:3098) ──► NestJS backend (:3097) ──► Postgres + Redis
The ports are deliberately off the dev defaults (3000/3200) so the suite can run beside your normal dev servers without stealing them.
Quick start
# one-time: services + schema
# Postgres and Redis running locally (docker-compose up -d postgres redis works)
cd backend && npm ci && npm run migration:run
# the suite
cd ../admin && npm ci
npx playwright install chromium
npm run e2e
That is the whole ritual. playwright.config.ts starts the backend and the
admin itself if they are not already listening (and reuses them if they are);
e2e/global-setup.ts then:
- waits for
/api/health(real DB + Redis check), - seeds deterministic data through the backend's own seed CLI
(
backend/src/database/seeds/e2e-seed.ts— see below), - signs in once through the real OTP endpoints and saves the session the same way the login page would (localStorage tokens).
Useful variations:
npx playwright test e2e/auth.spec.ts # one spec
npx playwright test --headed # watch it
E2E_SKIP_SEED=1 npm run e2e # skip the seed step (data already in place)
E2E_BACKEND_PORT=4001 E2E_ADMIN_PORT=4002 npm run e2e # other ports
E2E_ADMIN_PHONE=+15559550001 npm run e2e # non-India install (see below)
What the suite covers (21 tests)
- auth — interactive login by phone + dev OTP lands on the dashboard; a rider account gets an RBAC-stripped shell (and 403s from operator APIs); sign-out clears the session.
- dashboard — the four live KPI cards equal
/api/admin/stats; Revenue Today renders in the installation currency; the live-trips panel lists exactly the rides/api/admin/trips/livereturns. - trips — list count + first row match
/api/admin/trips; row click opens the detail (Trip #XXXXXXXX, full ride id); the status filter narrows the table to matching rides only. - dispatch — the operator console opens with the live queue (row per queue ride, awaiting-dispatch counter) and a real Leaflet map.
- drivers — fleet table matches
/api/admin/drivers; search narrows to a driver; the profile page shows that driver's real name and phone. - pricing — every vehicle type appears under Global fares; the Per-market fares tab renders the install market's overrides in its own currency.
- markets — the catalog count matches
/api/markets; the install's market row shows its currency and Active state. - parcels / organizations / safety — each queue matches its API and shows the seeded fixture (a delivered parcel, the corporate org, an open critical SOS + a dispute reachable through the kind filter).
- settings — the Localization card shows the DB-backed values
(
installation_settings, not.env); the language switcher offers exactly the market's languages. - i18n — switching to Hindi re-renders the live UI and survives a reload; an Arabic locale flips the whole document to RTL. (The Hindi test skips itself on installs whose market does not offer Hindi.)
- promo write flow — creates a promo through the modal, asserts it in the table AND via the API, deletes it through the confirm dialog, asserts it is gone from both. Reversible by design; an API cleanup runs even on failure.
Deterministic data — the e2e seed CLI
The suite never hand-inserts SQL. e2e/global-setup.ts runs:
cd backend
npx ts-node -r tsconfig-paths/register src/database/seeds/e2e-seed.ts
which is an extension of the standard npm run seed -- --demo: the same
bootstrap admin, the same demo drivers/riders/rides/ledger, plus the handful
of rows the E2E flows assert on (two safety incidents, one corporate org, one
delivered parcel). Everything is idempotent (UUIDv5 ids + ON CONFLICT DO NOTHING) and market-driven — an India install seeds ₹ and +91 phones, a
Brazil install seeds R$ and +55. It also resets the OTP request throttle for
the seeded demo phones (and only those) so back-to-back runs never trip the
real rate limit, and purges E2ESMOKE* promo rows earlier write-flow runs
soft-deleted.
Remove the E2E extras with:
npx ts-node -r tsconfig-paths/register src/database/seeds/e2e-seed.ts -- --clean
npm run seed -- --clean-demo # and the demo layer, if you want it gone too
Non-India installs
Assertions derive currency, market name, and languages from the API at
runtime, so the suite runs on any market. Two things are phone-shaped and
follow the seed: the admin login number (default +919955000001) and the
demo rider (+919855000001). On another market the seed derives them from
that market's dial code — pass E2E_ADMIN_PHONE / E2E_RIDER_PHONE
accordingly (the seed prints the admin phone when it runs).
What this layer does and does not prove
Proves: routing, auth, RBAC, i18n/RTL, the API contracts every page depends on, currency rendering, and one full write round-trip — against a real browser, backend, database and cache.
Does not prove: real SMS delivery (dev master OTP 000000 is active only
while no SMS provider is configured), real payment-gateway flows (provider
sandboxes need buyer credentials), websocket push timing under load, or
visual pixel-perfection. Those need provider sandboxes and visual-regression
tooling respectively.
2. Maestro — the mobile apps
mobile/rider/e2e/*.yaml and mobile/driver/e2e/*.yaml are
Maestro flows:
- rider — first-launch → language sheet (market languages) → OTP login → home (market-correct, e.g. the UPI chip on an India install) → destination search → a real fare quote (stops before booking) → wallet (₹ balance) → safety centre → ride history.
- driver — OTP login → offline home → go online (a real state change the admin dashboard sees) → back offline → earnings with the seeded settlement history.
They need a simulator or device with a dev-client build of the app — that is a property of mobile E2E, not of this repo. To run them:
# 1. backend running + seeded (same as the Playwright section)
# 2. build & boot the app on a simulator/emulator, pointed at the backend:
cd mobile/rider
EXPO_PUBLIC_API_URL=http://<your-lan-ip>:3097 npx expo run:ios # or run:android
# 3. the flows, in order (login first — later flows reuse the session):
curl -fsSL "https://get.maestro.mobile.dev" | bash # once
maestro test e2e/ # runs the folder in filename order
Same for mobile/driver. On Android emulators use http://10.0.2.2:3097.
Notes:
- The flows use the dev master OTP
000000and the seeded demo accounts (rider+919855000001, driver+919755000001on an India install). - The rider login flow makes one real OTP request; the seed CLI resets the per-phone throttle between runs.
- The booking flow stops at the fare quote on purpose: a booking would create
a live ride that needs a driver to resolve. The quote already proves
geocoding → routing → per-market fare math end to end. A deterministic
driver-offer assertion needs a scripted rider booking in range — wire that
through
POST /api/dispatcher/bookif you want to extend the driver flow. - Selector caveat: the apps expose very few
testID/accessibilityLabelhooks today, so the flows select by visible text (English catalog). Running them on a device set to another app language will fail — switch the app to English first, or add stable test IDs (tracked as an accessibility follow-up).
Validation vs execution: every flow file is validated with the real
Maestro CLI (maestro check-syntax) — locally during development and in the
CI job below. Executing them requires the simulator setup above and is a
local/device-farm activity; CI does not execute them.
3. What CI runs
.github/workflows/e2e.yml:
- playwright — on every pull request touching
admin/**orbackend/**: boots Postgres (PostGIS) + Redis service containers, migrates, lets the Playwright config start the backend and admin, seeds through the e2e seed CLI, and runs the full 21-test suite headless. Traces and screenshots are uploaded on failure. CI migrates withDEFAULT_COUNTRY=INso the fresh install matches the documented local setup. - maestro-flows —
workflow_dispatchonly: validates every mobile flow withmaestro check-syntax. Executing the flows in CI needs a macOS runner with booted simulators (or a device farm such as Maestro Cloud) plus per-PR dev-client builds of both apps — infrastructure the template does not presume. The job is the hook to wire yours into.
The unit layers run in their own existing workflows
(backend-ci.yml, admin-ci.yml, rider-ci.yml, driver-ci.yml).