Files
edr-platform/e2e/freight
Marshal 8ccb0e1558 Refactor booking process to use bookAndClear utility
- Replaced instances of bookContainers with bookAndClear across multiple test files to streamline booking and acceptance process.
- Updated import statements to include bookAndClear where necessary.
- Removed redundant acceptOperation calls after booking, as bookAndClear handles this internally.
- Adjusted comments and documentation to reflect changes in booking logic.
- Modified forceReservationExpiry function to ensure payment deadlines are set correctly, preventing issues with booking promotions.
2026-08-01 18:42:45 +00:00
..
2026-07-31 10:33:27 +00:00
2026-07-31 10:33:27 +00:00
2026-07-31 10:33:27 +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.