@edr/freight-e2e — Cypress e2e suite for the freight system
Containerized, fully isolated e2e environment: throwaway Postgres (tmpfs), MinIO, freight-api, portal, and backoffice — plus a Cypress runner that works both headless-in-Docker and interactively from the host against the same URLs.
Stack (docker-compose.e2e.yaml, project name edr-freight-e2e)
| Service | Default port | Notes |
|---|---|---|
freight-api-e2e |
3101 | migrations + seeders run at boot |
freight-portal-e2e |
5373 | nginx static build, API URL baked at build |
freight-backoffice-e2e |
5383 | nginx static build, API URL baked at build |
postgres-freight-e2e |
5533 | edr_freight_e2e, tmpfs — gone on down |
minio-e2e |
9310/9311 | object storage for file features |
cypress |
(host net) | profile cypress, headless chrome |
Ports are env-parameterized (E2E_API_PORT, E2E_PORTAL_PORT,
E2E_BACKOFFICE_PORT, E2E_DB_PORT, E2E_MINIO_PORT,
E2E_MINIO_CONSOLE_PORT). Defaults avoid the dev stacks; if a default is
busy anyway, the launcher scans upward for a free port, remembers the choice
in .e2e-ports.json (gitignored) while the stack is up, and passes matching
URLs to both compose and Cypress. The dev database is never touched.
Usage (from repo root)
One command — the launcher (scripts/e2e.mjs) auto-builds and starts the
stack if it isn't running, waits for healthchecks, then runs Cypress against
whatever ports were picked:
pnpm e2e:freight:run # headless run from the host (auto-up)
pnpm e2e:freight:open # interactive Cypress on the host (auto-up)
pnpm e2e:freight:ci # headless run inside the cypress container (auto-up)
pnpm e2e:freight:up # just start the stack
pnpm e2e:freight:down # teardown, drop all data + forget ports
pnpm e2e:freight:run --spec 'cypress/e2e/flows/**' # extra args → cypress
First up is slow (image builds + 240 migrations + seeders — healthcheck
allows 3 min). Later runs against a live stack skip docker entirely. Requires
the same root .npmrc (GitHub Packages auth for @tria-plc) as the main
compose file. Note: a non-default API port forces a web-image rebuild (the
API URL is baked into the static builds).
The cypress service uses network_mode: host (Linux). On macOS/Windows run
Cypress from the host (e2e:freight:open / e2e:freight:run) instead of the
container.
Test users
Inserted by Cypress itself — a global before() hook runs
cy.task("db:seedUsers"), which executes cypress/fixtures/seed-users.sql
then cypress/fixtures/seed-company.sql (idempotent, pre-hashed argon2
passwords) against the e2e database. No API code is involved; the app's user
seeders stay disabled. The API's always-on boot seeders must have run first
(org/unit/positions) — guaranteed once freight-api-e2e is healthy.
- Staff (backoffice):
linestaff|chief|director|ceo|marketer|operation|gl-et|gl-dj@edr.local— passwordpassword@tria - Customers (portal):
user@gmail.com,user2@gmail.com— password12345678
seed-company.sql additionally gives user@gmail.com an ACTIVE company
("E2E Logistics PLC", TIN 0102030405) with an approved importer profile —
the contract wizard's precondition — and grants chief the
edr_freight_app:admin permission (customer-profile approval is
FreightAdmin-guarded and no seeded position carries it otherwise).
Full map in cypress/fixtures/users.json.
Conventions
- Programmatic login everywhere except the two dedicated UI-login specs:
cy.loginBackoffice(email?)/cy.loginPortal(email?)—cy.session-cached (across specs),POST /api/auth/login, sets theauth-token/refresh-tokencookies the apps read. - Origins:
baseUrlis the backoffice (5383). Portal specscy.visitthe absolute portal URL; a test that touches both apps wraps portal steps incy.origin()(different port = different origin). Cookies ignore ports — always call the matching login command right before switching apps socy.sessionrestores the right cookie snapshot. - DB access:
cy.task("db:query", { sql, params })runs SQL against the e2e database (E2E_DB_URL, defaultlocalhost:5533). Use for seeding edge-case data and asserting side effects — it can never reach the dev DB. - OTPs: SMS/email delivery is disabled in e2e, but codes are still stored
in
freight.otp_verifications—cy.getOtp(emailOrPhone)polls them out. Used by signup verification and contract customer-signing. - Spec layout:
cypress/e2e/api/— API contract viacy.request(no browser)cypress/e2e/backoffice/— staff appcypress/e2e/portal/— customer appcypress/e2e/flows/— cross-app journeys (both directions):onboarding.cy.ts— signup → OTP → wizard (docs + license upload) → backoffice approval → customer can contractcontract-lifecycle.cy.ts— wizard → submit → accept → 2-step approval → PDF → customer OTP-sign → staff counter-sign →CONTRACT_ACTIVE
- Journey specs (
flows/onboarding,flows/contract-lifecycle) run withretries: 0and resolve mid-journey state (user, company, contract) from the DB at the start of each test: switching origin between tests reloads the spec bundle, so module-level variables do NOT survive across tests.
Extending
Deep module flows (booking wizard → staff approval → scheduling → billing)
belong in flows/. Pattern: arrange via API/db:query, act through the UI of
one app, assert through the UI of the other + a db:query cross-check. Prefer
adding data-testid attributes to app code over brittle text selectors.