mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
chore: enhance the tests
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -34,3 +34,6 @@ docker-compose.override.yml
|
|||||||
e2e/**/cypress/videos/
|
e2e/**/cypress/videos/
|
||||||
e2e/**/cypress/screenshots/
|
e2e/**/cypress/screenshots/
|
||||||
e2e/**/cypress/downloads/
|
e2e/**/cypress/downloads/
|
||||||
|
|
||||||
|
# e2e launcher state (ports of the running stack)
|
||||||
|
e2e/freight/.e2e-ports.json
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
# vanishes on `down`), seeded test users. Requires the same .npmrc as the main
|
# vanishes on `down`), seeded test users. Requires the same .npmrc as the main
|
||||||
# docker-compose.yaml (GitHub Packages auth for @tria-plc).
|
# docker-compose.yaml (GitHub Packages auth for @tria-plc).
|
||||||
#
|
#
|
||||||
# Up (build + wait healthy): docker compose -f docker-compose.e2e.yaml up -d --build --wait
|
# Preferred entrypoint: the launcher (auto-up + free-port picking):
|
||||||
# Headless run in container: docker compose -f docker-compose.e2e.yaml --profile cypress run --rm cypress
|
# pnpm e2e:freight:run|open|ci|up|down → e2e/freight/scripts/e2e.mjs
|
||||||
# Interactive from host: pnpm --filter @edr/freight-e2e cy:open
|
|
||||||
# Teardown: docker compose -f docker-compose.e2e.yaml down -v --remove-orphans
|
|
||||||
#
|
#
|
||||||
# Host ports (chosen to never collide with the dev stacks — 5273/5283/3221 are
|
# Host ports are env-parameterized (E2E_*_PORT). Defaults below avoid the dev
|
||||||
# taken by the second dev checkout in ~/projects/nathnael/edr-platform):
|
# stacks (5273/5283/3221 are taken by the second dev checkout in
|
||||||
|
# ~/projects/nathnael/edr-platform); when a default is busy the launcher scans
|
||||||
|
# upward for a free port and remembers the choice in e2e/freight/.e2e-ports.json
|
||||||
|
# while the stack is up:
|
||||||
# freight-api 3101 portal 5373 backoffice 5383
|
# freight-api 3101 portal 5373 backoffice 5383
|
||||||
# postgres 5533 minio 9310 (console 9311)
|
# postgres 5533 minio 9310 (console 9311)
|
||||||
name: edr-freight-e2e
|
name: edr-freight-e2e
|
||||||
@@ -24,7 +25,7 @@ services:
|
|||||||
tmpfs:
|
tmpfs:
|
||||||
- /var/lib/postgresql/data
|
- /var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- "5533:5432"
|
- "${E2E_DB_PORT:-5533}:5432"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U edr_e2e -d edr_freight_e2e"]
|
test: ["CMD-SHELL", "pg_isready -U edr_e2e -d edr_freight_e2e"]
|
||||||
interval: 2s
|
interval: 2s
|
||||||
@@ -40,8 +41,8 @@ services:
|
|||||||
tmpfs:
|
tmpfs:
|
||||||
- /data
|
- /data
|
||||||
ports:
|
ports:
|
||||||
- "9310:9000"
|
- "${E2E_MINIO_PORT:-9310}:9000"
|
||||||
- "9311:9001"
|
- "${E2E_MINIO_CONSOLE_PORT:-9311}:9001"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mc", "ready", "local"]
|
test: ["CMD", "mc", "ready", "local"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
@@ -107,9 +108,9 @@ services:
|
|||||||
# SMS strategy has no kill switch and defaults to a real dev endpoint —
|
# SMS strategy has no kill switch and defaults to a real dev endpoint —
|
||||||
# blackhole it so e2e never sends SMS (failures are logged, non-fatal).
|
# blackhole it so e2e never sends SMS (failures are logged, non-fatal).
|
||||||
OZIKING_SMS_URL: http://127.0.0.1:9/sms
|
OZIKING_SMS_URL: http://127.0.0.1:9/sms
|
||||||
FREIGHT_PORTAL_URL: http://localhost:5373
|
FREIGHT_PORTAL_URL: http://localhost:${E2E_PORTAL_PORT:-5373}
|
||||||
ports:
|
ports:
|
||||||
- "3101:3001"
|
- "${E2E_API_PORT:-3101}:3001"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
# Boot runs 240+ migrations + seeders on first start — generous start_period.
|
# Boot runs 240+ migrations + seeders on first start — generous start_period.
|
||||||
test:
|
test:
|
||||||
@@ -132,9 +133,10 @@ services:
|
|||||||
TURBO_FILTER: "@edr/freight-portal"
|
TURBO_FILTER: "@edr/freight-portal"
|
||||||
APP_PATH: apps/edr-freight-web/portal
|
APP_PATH: apps/edr-freight-web/portal
|
||||||
# Baked at build time: browser (host or host-networked cypress
|
# Baked at build time: browser (host or host-networked cypress
|
||||||
# container) reaches the API through the published host port.
|
# container) reaches the API through the published host port. A
|
||||||
VITE_API_URL: http://localhost:3101
|
# non-default API port therefore forces a web image rebuild.
|
||||||
VITE_BASE_API_URL: http://localhost:3101
|
VITE_API_URL: http://localhost:${E2E_API_PORT:-3101}
|
||||||
|
VITE_BASE_API_URL: http://localhost:${E2E_API_PORT:-3101}
|
||||||
VITE_USER_MANAGEMENT_BASE: /_um
|
VITE_USER_MANAGEMENT_BASE: /_um
|
||||||
VITE_GOOGLE_MAPS_API_KEY: ""
|
VITE_GOOGLE_MAPS_API_KEY: ""
|
||||||
VITE_POSTHOG_KEY: ""
|
VITE_POSTHOG_KEY: ""
|
||||||
@@ -142,7 +144,7 @@ services:
|
|||||||
secrets:
|
secrets:
|
||||||
- npmrc
|
- npmrc
|
||||||
ports:
|
ports:
|
||||||
- "5373:80"
|
- "${E2E_PORTAL_PORT:-5373}:80"
|
||||||
|
|
||||||
freight-backoffice-e2e:
|
freight-backoffice-e2e:
|
||||||
build:
|
build:
|
||||||
@@ -151,8 +153,8 @@ services:
|
|||||||
args:
|
args:
|
||||||
TURBO_FILTER: "@edr/freight-backoffice"
|
TURBO_FILTER: "@edr/freight-backoffice"
|
||||||
APP_PATH: apps/edr-freight-web/backoffice
|
APP_PATH: apps/edr-freight-web/backoffice
|
||||||
VITE_API_URL: http://localhost:3101
|
VITE_API_URL: http://localhost:${E2E_API_PORT:-3101}
|
||||||
VITE_BASE_API_URL: http://localhost:3101
|
VITE_BASE_API_URL: http://localhost:${E2E_API_PORT:-3101}
|
||||||
VITE_USER_MANAGEMENT_BASE: /_um
|
VITE_USER_MANAGEMENT_BASE: /_um
|
||||||
VITE_GOOGLE_MAPS_API_KEY: ""
|
VITE_GOOGLE_MAPS_API_KEY: ""
|
||||||
VITE_POSTHOG_KEY: ""
|
VITE_POSTHOG_KEY: ""
|
||||||
@@ -160,7 +162,7 @@ services:
|
|||||||
secrets:
|
secrets:
|
||||||
- npmrc
|
- npmrc
|
||||||
ports:
|
ports:
|
||||||
- "5383:80"
|
- "${E2E_BACKOFFICE_PORT:-5383}:80"
|
||||||
|
|
||||||
# Headless runner — opt-in via `--profile cypress`. host network so the
|
# Headless runner — opt-in via `--profile cypress`. host network so the
|
||||||
# in-container browser uses the exact same localhost URLs as `cypress open`
|
# in-container browser uses the exact same localhost URLs as `cypress open`
|
||||||
@@ -180,7 +182,10 @@ services:
|
|||||||
entrypoint: ["cypress", "run", "--browser", "chrome"]
|
entrypoint: ["cypress", "run", "--browser", "chrome"]
|
||||||
environment:
|
environment:
|
||||||
CI: "true"
|
CI: "true"
|
||||||
E2E_DB_URL: postgres://edr_e2e:edr_e2e@localhost:5533/edr_freight_e2e
|
E2E_DB_URL: postgres://edr_e2e:edr_e2e@localhost:${E2E_DB_PORT:-5533}/edr_freight_e2e
|
||||||
|
CYPRESS_BASE_URL: http://localhost:${E2E_BACKOFFICE_PORT:-5383}
|
||||||
|
CYPRESS_API_URL: http://localhost:${E2E_API_PORT:-3101}
|
||||||
|
CYPRESS_PORTAL_URL: http://localhost:${E2E_PORTAL_PORT:-5373}
|
||||||
volumes:
|
volumes:
|
||||||
- .:/repo
|
- .:/repo
|
||||||
|
|
||||||
|
|||||||
@@ -6,31 +6,42 @@ both headless-in-Docker and interactively from the host against the same URLs.
|
|||||||
|
|
||||||
## Stack (`docker-compose.e2e.yaml`, project name `edr-freight-e2e`)
|
## Stack (`docker-compose.e2e.yaml`, project name `edr-freight-e2e`)
|
||||||
|
|
||||||
| Service | Host port | Notes |
|
| Service | Default port | Notes |
|
||||||
| ----------------------- | --------- | ---------------------------------------------- |
|
| ----------------------- | ------------ | ---------------------------------------------- |
|
||||||
| `freight-api-e2e` | 3101 | migrations + seeders run at boot |
|
| `freight-api-e2e` | 3101 | migrations + seeders run at boot |
|
||||||
| `freight-portal-e2e` | 5373 | nginx static build, API baked to `:3101` |
|
| `freight-portal-e2e` | 5373 | nginx static build, API URL baked at build |
|
||||||
| `freight-backoffice-e2e`| 5383 | nginx static build, API baked to `:3101` |
|
| `freight-backoffice-e2e`| 5383 | nginx static build, API URL baked at build |
|
||||||
| `postgres-freight-e2e` | 5533 | `edr_freight_e2e`, tmpfs — gone on `down` |
|
| `postgres-freight-e2e` | 5533 | `edr_freight_e2e`, tmpfs — gone on `down` |
|
||||||
| `minio-e2e` | 9310/9311 | object storage for file features |
|
| `minio-e2e` | 9310/9311 | object storage for file features |
|
||||||
| `cypress` | (host net)| profile `cypress`, headless chrome |
|
| `cypress` | (host net) | profile `cypress`, headless chrome |
|
||||||
|
|
||||||
Ports deliberately avoid the dev stack (3001/5173/5183/5433). The dev database
|
Ports are env-parameterized (`E2E_API_PORT`, `E2E_PORTAL_PORT`,
|
||||||
is never touched.
|
`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)
|
## 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
|
```bash
|
||||||
pnpm e2e:freight:up # build + start stack, wait for healthchecks
|
pnpm e2e:freight:run # headless run from the host (auto-up)
|
||||||
pnpm e2e:freight:open # interactive Cypress on the host
|
pnpm e2e:freight:open # interactive Cypress on the host (auto-up)
|
||||||
pnpm e2e:freight:run # headless run from the host
|
pnpm e2e:freight:ci # headless run inside the cypress container (auto-up)
|
||||||
pnpm e2e:freight:ci # headless run inside the cypress container
|
pnpm e2e:freight:up # just start the stack
|
||||||
pnpm e2e:freight:down # teardown, drop all data
|
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
|
First `up` is slow (image builds + 240 migrations + seeders — healthcheck
|
||||||
allows 3 min). Requires the same root `.npmrc` (GitHub Packages auth for
|
allows 3 min). Later runs against a live stack skip docker entirely. Requires
|
||||||
`@tria-plc`) as the main compose file.
|
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
|
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
|
Cypress from the host (`e2e:freight:open` / `e2e:freight:run`) instead of the
|
||||||
@@ -39,10 +50,10 @@ container.
|
|||||||
## Test users
|
## Test users
|
||||||
|
|
||||||
Inserted by Cypress itself — a global `before()` hook runs
|
Inserted by Cypress itself — a global `before()` hook runs
|
||||||
`cy.task("db:seedUsers")`, which executes
|
`cy.task("db:seedUsers")`, which executes `cypress/fixtures/seed-users.sql`
|
||||||
`cypress/fixtures/seed-users.sql` (idempotent, pre-hashed argon2 passwords)
|
then `cypress/fixtures/seed-company.sql` (idempotent, pre-hashed argon2
|
||||||
against the e2e database. No API code is involved; the app's user seeders
|
passwords) against the e2e database. No API code is involved; the app's user
|
||||||
stay disabled. The API's always-on boot seeders must have run first
|
seeders stay disabled. The API's always-on boot seeders must have run first
|
||||||
(org/unit/positions) — guaranteed once `freight-api-e2e` is healthy.
|
(org/unit/positions) — guaranteed once `freight-api-e2e` is healthy.
|
||||||
|
|
||||||
- Staff (backoffice): `linestaff|chief|director|ceo|marketer|operation|gl-et|gl-dj@edr.local`
|
- Staff (backoffice): `linestaff|chief|director|ceo|marketer|operation|gl-et|gl-dj@edr.local`
|
||||||
@@ -50,6 +61,12 @@ stay disabled. The API's always-on boot seeders must have run first
|
|||||||
- Customers (portal): `user@gmail.com`, `user2@gmail.com`
|
- Customers (portal): `user@gmail.com`, `user2@gmail.com`
|
||||||
— password `12345678`
|
— 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`.
|
Full map in `cypress/fixtures/users.json`.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
@@ -66,11 +83,22 @@ Full map in `cypress/fixtures/users.json`.
|
|||||||
- **DB access**: `cy.task("db:query", { sql, params })` runs SQL against the
|
- **DB access**: `cy.task("db:query", { sql, params })` runs SQL against the
|
||||||
e2e database (`E2E_DB_URL`, default `localhost:5533`). Use for seeding
|
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.
|
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**:
|
- **Spec layout**:
|
||||||
- `cypress/e2e/api/` — API contract via `cy.request` (no browser)
|
- `cypress/e2e/api/` — API contract via `cy.request` (no browser)
|
||||||
- `cypress/e2e/backoffice/` — staff app
|
- `cypress/e2e/backoffice/` — staff app
|
||||||
- `cypress/e2e/portal/` — customer app
|
- `cypress/e2e/portal/` — customer app
|
||||||
- `cypress/e2e/flows/` — cross-app journeys (both directions)
|
- `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.
|
||||||
|
|
||||||
## Extending
|
## Extending
|
||||||
|
|
||||||
|
|||||||
@@ -190,7 +190,10 @@ describe("contract lifecycle: creation to finalization", { retries: 0 }, () => {
|
|||||||
cy.get(`[id="${id}"]`).clear().type("EDR Marketer");
|
cy.get(`[id="${id}"]`).clear().type("EDR Marketer");
|
||||||
});
|
});
|
||||||
cy.drawSignature();
|
cy.drawSignature();
|
||||||
cy.contains("button", /^Confirm signature$|^Approve & sign$/).click();
|
// Scoped to the modal — the toolbar behind it has its own "Approve & sign".
|
||||||
|
cy.get(".mantine-Modal-content")
|
||||||
|
.contains("button", /^Confirm signature$|^Approve & sign$/)
|
||||||
|
.click();
|
||||||
|
|
||||||
cy.contains("counter-signed", { timeout: 30000 }).should("be.visible");
|
cy.contains("counter-signed", { timeout: 30000 }).should("be.visible");
|
||||||
|
|
||||||
|
|||||||
193
e2e/freight/scripts/e2e.mjs
Normal file
193
e2e/freight/scripts/e2e.mjs
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Freight e2e launcher — one command, no manual steps:
|
||||||
|
*
|
||||||
|
* node e2e/freight/scripts/e2e.mjs <up|run|open|ci|down> [cypress args...]
|
||||||
|
*
|
||||||
|
* - run/open/ci auto-start the docker stack (build + wait healthy) if it
|
||||||
|
* isn't already running, then launch Cypress pointed at the right ports.
|
||||||
|
* - Host ports default to 3101/5373/5383/5533/9310/9311; any default that is
|
||||||
|
* busy is replaced by the next free port. Chosen ports are written to
|
||||||
|
* e2e/freight/.e2e-ports.json (gitignored) and reused while the stack is
|
||||||
|
* up, so cypress and compose always agree.
|
||||||
|
* - Extra args are forwarded to Cypress: `pnpm e2e:freight:run --spec ...`.
|
||||||
|
*
|
||||||
|
* No dependencies — plain Node, spawns `docker compose` and `pnpm`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execFileSync, spawnSync } from "node:child_process";
|
||||||
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { createServer } from "node:net";
|
||||||
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const e2eDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const repoRoot = resolve(e2eDir, "..", "..");
|
||||||
|
const stateFile = join(e2eDir, ".e2e-ports.json");
|
||||||
|
const composeBase = ["compose", "-f", join(repoRoot, "docker-compose.e2e.yaml")];
|
||||||
|
|
||||||
|
const DEFAULT_PORTS = {
|
||||||
|
E2E_API_PORT: 3101,
|
||||||
|
E2E_PORTAL_PORT: 5373,
|
||||||
|
E2E_BACKOFFICE_PORT: 5383,
|
||||||
|
E2E_DB_PORT: 5533,
|
||||||
|
E2E_MINIO_PORT: 9310,
|
||||||
|
E2E_MINIO_CONSOLE_PORT: 9311,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Long-running services that must be up before tests (minio-init exits).
|
||||||
|
const SERVICES = [
|
||||||
|
"postgres-freight-e2e",
|
||||||
|
"minio-e2e",
|
||||||
|
"freight-api-e2e",
|
||||||
|
"freight-portal-e2e",
|
||||||
|
"freight-backoffice-e2e",
|
||||||
|
];
|
||||||
|
|
||||||
|
function fail(msg) {
|
||||||
|
console.error(`\ne2e: ${msg}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function preflight() {
|
||||||
|
try {
|
||||||
|
execFileSync("docker", ["info"], { stdio: "ignore" });
|
||||||
|
} catch {
|
||||||
|
fail("docker is not running (or not installed) — start Docker and retry.");
|
||||||
|
}
|
||||||
|
if (!existsSync(join(repoRoot, ".npmrc"))) {
|
||||||
|
fail(
|
||||||
|
".npmrc missing at repo root — image builds need GitHub Packages auth " +
|
||||||
|
"for @tria-plc (same file the main docker-compose.yaml uses).",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPortFree(port) {
|
||||||
|
return new Promise((res) => {
|
||||||
|
const srv = createServer();
|
||||||
|
srv.once("error", () => res(false));
|
||||||
|
srv.once("listening", () => srv.close(() => res(true)));
|
||||||
|
srv.listen(port);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stackRunning(env) {
|
||||||
|
try {
|
||||||
|
const out = execFileSync(
|
||||||
|
"docker",
|
||||||
|
[...composeBase, "ps", "--services", "--status", "running"],
|
||||||
|
{ encoding: "utf8", env, stdio: ["ignore", "pipe", "ignore"] },
|
||||||
|
);
|
||||||
|
const running = new Set(out.split("\n").filter(Boolean));
|
||||||
|
return SERVICES.every((s) => running.has(s));
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* While the stack runs, ports are whatever it was started with (state file,
|
||||||
|
* else the defaults — a hand-started stack used the compose defaults). Only a
|
||||||
|
* fresh start gets to scan for free ports.
|
||||||
|
*/
|
||||||
|
async function resolvePorts() {
|
||||||
|
if (stackRunning(process.env)) {
|
||||||
|
return existsSync(stateFile)
|
||||||
|
? JSON.parse(readFileSync(stateFile, "utf8"))
|
||||||
|
: { ...DEFAULT_PORTS };
|
||||||
|
}
|
||||||
|
const ports = {};
|
||||||
|
const taken = new Set();
|
||||||
|
for (const [name, preferred] of Object.entries(DEFAULT_PORTS)) {
|
||||||
|
let port = preferred;
|
||||||
|
while (taken.has(port) || !(await isPortFree(port))) port += 1;
|
||||||
|
taken.add(port);
|
||||||
|
ports[name] = port;
|
||||||
|
if (port !== preferred)
|
||||||
|
console.log(`e2e: port ${preferred} busy → ${name}=${port}`);
|
||||||
|
}
|
||||||
|
return ports;
|
||||||
|
}
|
||||||
|
|
||||||
|
function envFor(ports) {
|
||||||
|
return {
|
||||||
|
...process.env,
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(ports).map(([k, v]) => [k, String(v)]),
|
||||||
|
),
|
||||||
|
CYPRESS_BASE_URL: `http://localhost:${ports.E2E_BACKOFFICE_PORT}`,
|
||||||
|
CYPRESS_API_URL: `http://localhost:${ports.E2E_API_PORT}`,
|
||||||
|
CYPRESS_PORTAL_URL: `http://localhost:${ports.E2E_PORTAL_PORT}`,
|
||||||
|
E2E_DB_URL: `postgres://edr_e2e:edr_e2e@localhost:${ports.E2E_DB_PORT}/edr_freight_e2e`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compose(args, env) {
|
||||||
|
const { status } = spawnSync("docker", [...composeBase, ...args], {
|
||||||
|
stdio: "inherit",
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
return status ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function up(ports, env) {
|
||||||
|
preflight();
|
||||||
|
console.log(
|
||||||
|
`e2e: starting stack — api :${ports.E2E_API_PORT} portal :${ports.E2E_PORTAL_PORT} backoffice :${ports.E2E_BACKOFFICE_PORT} db :${ports.E2E_DB_PORT}`,
|
||||||
|
);
|
||||||
|
const status = compose(["up", "-d", "--build", "--wait"], env);
|
||||||
|
if (status !== 0)
|
||||||
|
fail(
|
||||||
|
"stack failed to become healthy. Inspect with:\n" +
|
||||||
|
" docker compose -f docker-compose.e2e.yaml logs freight-api-e2e",
|
||||||
|
);
|
||||||
|
writeFileSync(stateFile, JSON.stringify(ports, null, 2) + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureUp(ports, env) {
|
||||||
|
if (stackRunning(env)) return;
|
||||||
|
up(ports, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runPnpm(script, extra, env) {
|
||||||
|
const { status } = spawnSync(
|
||||||
|
"pnpm",
|
||||||
|
["--filter", "@edr/freight-e2e", "run", script, ...extra],
|
||||||
|
{ cwd: repoRoot, stdio: "inherit", env },
|
||||||
|
);
|
||||||
|
process.exit(status ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [cmd, ...extra] = process.argv.slice(2);
|
||||||
|
const ports = await resolvePorts();
|
||||||
|
const env = envFor(ports);
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case "up":
|
||||||
|
up(ports, env);
|
||||||
|
break;
|
||||||
|
case "run":
|
||||||
|
ensureUp(ports, env);
|
||||||
|
runPnpm("cy:run", extra, env);
|
||||||
|
break;
|
||||||
|
case "open":
|
||||||
|
ensureUp(ports, env);
|
||||||
|
runPnpm("cy:open", extra, env);
|
||||||
|
break;
|
||||||
|
case "ci":
|
||||||
|
ensureUp(ports, env);
|
||||||
|
process.exit(compose(["--profile", "cypress", "run", "--rm", "cypress", ...extra], env));
|
||||||
|
break;
|
||||||
|
case "down":
|
||||||
|
process.exit(
|
||||||
|
(() => {
|
||||||
|
const status = compose(["down", "-v", "--remove-orphans"], env);
|
||||||
|
rmSync(stateFile, { force: true });
|
||||||
|
return status;
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fail(`unknown command "${cmd ?? ""}" — use up | run | open | ci | down`);
|
||||||
|
}
|
||||||
10
package.json
10
package.json
@@ -23,11 +23,11 @@
|
|||||||
"format": "prettier --write \"**/*.{ts,tsx,json,md}\"",
|
"format": "prettier --write \"**/*.{ts,tsx,json,md}\"",
|
||||||
"docker:build": "docker compose build",
|
"docker:build": "docker compose build",
|
||||||
"docker:up": "docker compose up -d",
|
"docker:up": "docker compose up -d",
|
||||||
"e2e:freight:up": "docker compose -f docker-compose.e2e.yaml up -d --build --wait",
|
"e2e:freight:up": "node e2e/freight/scripts/e2e.mjs up",
|
||||||
"e2e:freight:open": "pnpm --filter @edr/freight-e2e cy:open",
|
"e2e:freight:open": "node e2e/freight/scripts/e2e.mjs open",
|
||||||
"e2e:freight:run": "pnpm --filter @edr/freight-e2e cy:run",
|
"e2e:freight:run": "node e2e/freight/scripts/e2e.mjs run",
|
||||||
"e2e:freight:ci": "docker compose -f docker-compose.e2e.yaml --profile cypress run --rm cypress",
|
"e2e:freight:ci": "node e2e/freight/scripts/e2e.mjs ci",
|
||||||
"e2e:freight:down": "docker compose -f docker-compose.e2e.yaml down -v --remove-orphans",
|
"e2e:freight:down": "node e2e/freight/scripts/e2e.mjs down",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Reference in New Issue
Block a user