Five journeys, one file each, every one of them crossing from the portal into
the backoffice and cross-checking the database rather than the screen:
- ethiopian eTrade verified, Fayda, approved, contract wizard reachable —
including the dead end a TIN with no trade licence is for an
ordinary company
- investor foreign investment licence: nothing on file at eTrade, typed
registration, passport identity, per-role licence still owed,
and the backoffice's manual-entry badge and banner
- cooperative no foreign option, no freight-forwarder role, the co-operative
document set, no licence cards, its own badge and banner
- switch_back settings → switch to eTrade → registration cleared, company
pending, wizard reopened on the company step → re-run through
eTrade → the flag is gone from the backoffice
- guards the refused combinations, and the mid-wizard un-tick that has
to clear the typed registration
Replaces the old onboarding.cy.ts (removed a commit earlier by accident of a
staged deletion): it drove a wizard shape that no longer exists — Fayda before
the company step, a "Personnel" step — so it could only ever have been red.
Notes for whoever edits these next. Attach files to the FIRST empty dropzone,
never by index — SmartFileInput removes the input once a file is on it. Resolve
the company from the database after any cross-origin hop, never from module
state: Cypress re-evaluates the spec bundle and Date.now() with it, which is
what latestJourney's run-stamp cutoff is for. And the deliberate eTrade 400 is
ignored as an uncaught exception — the portal handles that outcome on screen
but leaves the rejected request unhandled at the promise level.
@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.
The 40-scenario suite (flows/g1_* … flows/g10_*)
S1–S40 from the scenario document, one file per group after Group 1:
| File | Scenarios | Subject |
|---|---|---|
g1_s1_expiry_promotes_waitlist.cy.ts |
S1 | expiry frees exactly the waitlist's space |
g1_s2_exact_fill.cy.ts |
S2 | four bookings fill the train to the slot |
g1_s3_underfill_day_stays_open.cy.ts |
S3 | under-filled day stays bookable |
g1_s4_split_closes_gap.cy.ts |
S4 | a split closes the last gap |
g1_s5_cascading_expiry.cy.ts |
S5 | one expiry cascades into a second promotion |
g1_s6_s8_offers_and_priority.cy.ts |
S6–S8 | declined split, government preemption, priority tiers |
g2_weight.cy.ts |
S9–S12 | weight vs slots, and the tolerance rules |
g3_export.cy.ts |
S13–S18 | export FCFS, whole-or-nothing |
g4_multi_schedule.cy.ts |
S19–S21 | two trains on one day |
g5_waitlist.cy.ts |
S22–S24 | recovery: rebooking, split remainders, queue walking |
g6_corridor.cy.ts |
S25–S29 | the run: alighting, tracking, checkpoints |
g7_disruptions.cy.ts |
S30–S33 | cancel, wagon shortage, breakage, under-filled dispatch |
g8_import_customs.cy.ts |
S34–S37 | the clearance chain |
g9_delivery.cy.ts |
S38–S39 | self-haul trucks and last-mile |
g10_validation.cy.ts |
S40 | line validation and the parked re-priced booking |
Read SCENARIO_ENGINE_NOTES.md before changing any of these. Five
scenarios describe behaviour the engine does not implement (out-of-order
checkpoints, second-duty gating, hazardous/reefer clamping) or invert what it
does (mid-corridor intercity). Those are written as a passing test of CURRENT
behaviour plus an adjacent it.skip naming the desired behaviour — un-skipping
one is the definition of done for the corresponding fix, not a test repair.
Three specs are also flag- or policy-dependent and say so in their headers:
g3_export needs FREIGHT_EXPORT_SPLIT off, and g4_multi_schedule asserts
the whole-placement policy (S20) rather than the fill-first one (S21).
Group 1 conventions (flows/g1_*.cy.ts)
The visual counterpart to the corridor suite — helpers in flows/g1-utils.ts,
arrange-data in fixtures/seed-g1-train.sql (run it AFTER
seed-import-corridor.sql).
Two things make these different from the older flow specs:
- A 53-wagon BUILT train (
TRN-G1-1), not a loco pair. A loco-pair schedule cannot hold 53:syncScheduleMaxWagonsrecomputesmax_wagonsfrom locomotive length (floor(760 / 13.966) = 54on this corridor). A built train's physical consist wins outright — seebooking-batch.service.ts:4152. The consist staff marshal IS the capacity. - The configuration phase and every capacity verdict run through the UI:
the consist is seen in the Train Builder, the schedule is created through
the real "New schedule" form, the batch is run from the
Doc review complete — run batchbutton, and FULL/NOT FULL is read off the batch board's Priority Tracking tab — which renders the literalCapacity line · 53/53 wagons · FULLdivider plusIn the batch/Waiting list/Expiredlanes.
Bulk cargo still goes through the API (bookContainers): a 30-wagon booking
is 30-60 ISO-number inputs, which tests the form rather than the engine. Each
scenario books its ONE small booking visually via
bookContainersVisually(). Payment is always API-driven — the portal has
no mock payment path; "Pay now" redirects off-origin to a real gateway, which
Cypress cannot follow.
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.