Files
edr-platform/e2e/freight
Nathnael 333232c4d9 fix(portal): persist the region a manual-registration company picks
The Region select called setValue without shouldDirty. `region` is an
eTrade-bundle key, and stepPayload sends those only when the customer changed
them this session — so for the two routes that type their address by hand (a
co-operative, a foreign investor) the region was dropped on every save while
zone, woreda and kebele went through, because those are registered inputs and
are dirty by construction.

Found by the new onboarding e2e suite: both manual-route companies finished
onboarding with zone/woreda/kebele on file and region empty.
2026-08-18 11:37:43 +00:00
..
2026-07-31 10:33:27 +00:00
2026-07-31 10:33:27 +00:00
2026-08-02 10:26:36 +00:00
2026-08-02 10:26:36 +00:00

@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 — password password@tria
  • Customers (portal): user@gmail.com, user2@gmail.com — password 12345678

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 the auth-token / refresh-token cookies the apps read.
  • Origins: baseUrl is the backoffice (5383). Portal specs cy.visit the absolute portal URL; a test that touches both apps wraps portal steps in cy.origin() (different port = different origin). Cookies ignore ports — always call the matching login command right before switching apps so cy.session restores the right cookie snapshot.
  • DB access: cy.task("db:query", { sql, params }) runs SQL against the e2e database (E2E_DB_URL, default localhost: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_verificationscy.getOtp(emailOrPhone) polls them out. Used by signup verification and contract customer-signing.
  • Spec layout:
    • cypress/e2e/api/ — API contract via cy.request (no browser)
    • cypress/e2e/backoffice/ — staff app
    • cypress/e2e/portal/ — customer app
    • cypress/e2e/flows/ — cross-app journeys (both directions):
      • onboarding.cy.ts — signup → OTP → wizard (docs + license upload) → backoffice approval → customer can contract
      • contract-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 with retries: 0 and 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_*)

S1S40 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 S6S8 declined split, government preemption, priority tiers
g2_weight.cy.ts S9S12 weight vs slots, and the tolerance rules
g3_export.cy.ts S13S18 export FCFS, whole-or-nothing
g4_multi_schedule.cy.ts S19S21 two trains on one day
g5_waitlist.cy.ts S22S24 recovery: rebooking, split remainders, queue walking
g6_corridor.cy.ts S25S29 the run: alighting, tracking, checkpoints
g7_disruptions.cy.ts S30S33 cancel, wagon shortage, breakage, under-filled dispatch
g8_import_customs.cy.ts S34S37 the clearance chain
g9_delivery.cy.ts S38S39 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: syncScheduleMaxWagons recomputes max_wagons from locomotive length (floor(760 / 13.966) = 54 on this corridor). A built train's physical consist wins outright — see booking-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 batch button, and FULL/NOT FULL is read off the batch board's Priority Tracking tab — which renders the literal Capacity line · 53/53 wagons · FULL divider plus In the batch / Waiting list / Expired lanes.

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.