mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
101 lines
4.8 KiB
TypeScript
101 lines
4.8 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
import { API_URL, STATIONS, staffToken } from "../../fixtures/data";
|
|
import { UI_IDS } from "../../../apps/edr-passenger-api/test/fixtures/seed-ui";
|
|
|
|
/**
|
|
* Track B — per-station check-in cutoff, driven by the backoffice route config API. Follows the
|
|
* same convention as config-validation.spec.ts: hits the passenger-api directly with a staff
|
|
* bearer token rather than driving the real DOM — the route-stop form has no data-testid hooks,
|
|
* so browser automation here would be selector-fragile for no extra coverage value.
|
|
*
|
|
* Creates its OWN route (not the shared ROUTE_ID from seed-ui) since updateRoute deletes and
|
|
* recreates all stops — mutating the shared fixture route would break every other spec that
|
|
* depends on its distances/times staying stable for the whole suite run.
|
|
*/
|
|
function auth() {
|
|
return { Authorization: `Bearer ${staffToken()}` };
|
|
}
|
|
|
|
test("BC-11 ✅ travelMinutesToStop drives each stop's estimated arrival independently of route-wide departure", async ({ request }) => {
|
|
const routeRes = await request.post(`${API_URL}/routes`, {
|
|
headers: auth(),
|
|
data: {
|
|
code: `E2E-CUTOFF-${Date.now()}`,
|
|
name: "E2E Check-in Cutoff Route",
|
|
effectiveFrom: "2020-01-01T00:00:00Z",
|
|
stops: [
|
|
{ stationId: STATIONS.A, sequence: 1 },
|
|
{ stationId: STATIONS.B, sequence: 2, travelMinutesToStop: 60 },
|
|
{ stationId: STATIONS.C, sequence: 3, travelMinutesToStop: 40 },
|
|
],
|
|
},
|
|
});
|
|
expect(routeRes.ok()).toBeTruthy();
|
|
const route = (await routeRes.json())?.data ?? (await routeRes.json());
|
|
const routeId = route.id;
|
|
|
|
// Reuse the seeded coach via a route coach template so schedule creation auto-assigns real
|
|
// seats (createSchedule auto-applies any route coach template — see schedules.service.ts).
|
|
const templateRes = await request.put(`${API_URL}/routes/${routeId}/coaches`, {
|
|
headers: auth(),
|
|
data: { coaches: [{ coachId: UI_IDS.coach, positionNumber: 1 }] },
|
|
});
|
|
expect(templateRes.ok()).toBeTruthy();
|
|
|
|
// dep only 5 min out: A's cutoff (dep - 30min default) is already ~25 min in the past by the
|
|
// time this schedule is queried, but B's arrival (dep + 60min) keeps its own cutoff (arrival -
|
|
// 30min default) about 35 min in the future — proving the two stations close independently.
|
|
const dep = new Date(Date.now() + 5 * 60_000);
|
|
const arr = new Date(dep.getTime() + 100 * 60_000); // A->B 60min + B->C 40min
|
|
const scheduleRes = await request.post(`${API_URL}/schedules`, {
|
|
headers: auth(),
|
|
data: { trainId: UI_IDS.train, routeId, departureAt: dep.toISOString(), arrivalAt: arr.toISOString() },
|
|
});
|
|
expect(scheduleRes.ok()).toBeTruthy();
|
|
const schedule = (await scheduleRes.json())?.data ?? (await scheduleRes.json());
|
|
|
|
const getRes = await request.get(`${API_URL}/schedules/${schedule.id}`, { headers: auth() });
|
|
expect(getRes.ok()).toBeTruthy();
|
|
const full = (await getRes.json())?.data ?? (await getRes.json());
|
|
const stopTimes: any[] = full.stopTimes ?? [];
|
|
const bStop = stopTimes.find((s) => s.stationId === STATIONS.B);
|
|
const cStop = stopTimes.find((s) => s.stationId === STATIONS.C);
|
|
|
|
expect(new Date(bStop.plannedArrivalAt).getTime()).toBe(dep.getTime() + 60 * 60_000);
|
|
expect(new Date(cStop.plannedArrivalAt).getTime()).toBe(arr.getTime()); // last stop locked to overall arrival
|
|
|
|
// A's own segment (no arrival — falls back to its departure) is already past its cutoff...
|
|
const rejectRes = await request.post(`${API_URL}/seats/hold`, {
|
|
headers: auth(),
|
|
data: {
|
|
scheduleId: schedule.id,
|
|
originStationId: STATIONS.A,
|
|
destinationStationId: STATIONS.B,
|
|
passengers: [{ passengerId: "44444444-4444-4444-8444-444444444444", seatId: "00000000-0000-4000-8000-000000009999" }],
|
|
},
|
|
});
|
|
expect(rejectRes.status()).toBe(400);
|
|
expect((await rejectRes.json())?.message ?? "").toMatch(/cannot be held within/i);
|
|
|
|
// ...while B, whose own arrival is comfortably later, remains independently bookable.
|
|
const seatmapRes = await request.get(`${API_URL}/seats/seatmap/${schedule.id}`, { headers: auth() });
|
|
expect(seatmapRes.ok()).toBeTruthy();
|
|
const seatmap = (await seatmapRes.json())?.data ?? (await seatmapRes.json());
|
|
const seatId = seatmap.coaches?.[0]?.seats?.[0]?.id;
|
|
expect(seatId).toBeTruthy();
|
|
|
|
const holdRes = await request.post(`${API_URL}/seats/hold`, {
|
|
headers: auth(),
|
|
data: {
|
|
scheduleId: schedule.id,
|
|
originStationId: STATIONS.B,
|
|
destinationStationId: STATIONS.C,
|
|
passengers: [{ passengerId: "55555555-5555-4555-8555-555555555555", seatId }],
|
|
},
|
|
});
|
|
expect(holdRes.ok()).toBeTruthy();
|
|
|
|
await request.delete(`${API_URL}/schedules/${schedule.id}`, { headers: auth() }).catch(() => {});
|
|
await request.delete(`${API_URL}/routes/${routeId}?cascade=true`, { headers: auth() }).catch(() => {});
|
|
});
|