Files
edr-platform/e2e/freight/README.md
Marshal 123f925cc1 add Group 9 delivery scenarios for self-haul and last-mile paths
- Implemented G9·S38 tests for customer self-haul truck assignments, ensuring compliance with container limits and truck assignments.
- Added G9·S39 tests for last-mile delivery, self-haul, and yard pickup, verifying independent paths for multiple bookings on the same train.
- Created seed data for Group 1 and Group 2 scenarios, ensuring proper setup for weight and capacity tests.
- Updated booking interface to deprecate  in favor of  for better clarity in allocations.
2026-08-01 12:14:37 +00:00

170 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# @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:
```bash
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_verifications``cy.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.