import { test, expect } from "@playwright/test"; import { API_URL, STATIONS, resultsUrl, sampleDepartDate, staffToken } from "../../fixtures/data"; import { UI_IDS } from "../../../apps/edr-passenger-api/test/fixtures/seed-ui"; /** * Portal search empty-state — asserts /search's outboundReason drives a message specific to WHY * zero results came back, instead of the old one-size-fits-all "No trains available" copy (see * SearchService.classifyEmptySearch + results/page.tsx's emptyReasonCopy). Each test isolates its * own route/station/schedule (never mutates the shared seed fixtures) so it can run alongside the * rest of the portal suite without disturbing other specs' assumptions about the seeded trip. */ function auth() { return { Authorization: `Bearer ${staffToken()}` }; } /** Days-from-now at 06:00Z, matching seed-ui's sampleDepartAt() convention (same-day in Addis TZ). */ function daysFromNowDate(days: number): string { const d = new Date(); d.setUTCDate(d.getUTCDate() + days); d.setUTCHours(6, 0, 0, 0); return d.toISOString().slice(0, 10); } /** resultsUrl() hardcodes STATIONS.A→C and sampleDepartDate() — these tests need other pairs/dates. */ function customResultsUrl(originId: string, destinationId: string, date: string, adults = 1) { const p = new URLSearchParams({ origin: originId, destination: destinationId, date, tripType: "ONE_WAY", adults: String(adults), children: "0", nationality: "ETHIOPIAN", }); return `/booking/results?${p.toString()}`; } test("empty-state: fully booked shows a fully-booked message, not a generic one", async ({ page }) => { // The seeded trip (UI_IDS.schedule) has 48 seats total — asking for far more than that guarantees // zero seat classes satisfy the party size, without needing to actually consume real seats. await page.goto(resultsUrl({ adults: 500 }), { waitUntil: "domcontentloaded" }); await expect(page.getByRole("heading", { name: "Fully booked" })).toBeVisible({ timeout: 20_000 }); await expect(page.getByText(/fully booked on/i)).toBeVisible(); }); test("empty-state: no schedule that day shows the generic no-trains message", async ({ page }) => { // ROUTE_ID/STATIONS A→C is a real, active route — just pick a date far enough out that no // schedule was ever created for it. const farDate = daysFromNowDate(300); await page.goto(customResultsUrl(STATIONS.A, STATIONS.C, farDate), { waitUntil: "domcontentloaded" }); await expect(page.getByRole("heading", { name: "No trains available" })).toBeVisible({ timeout: 20_000 }); await expect(page.getByText(/no trains available on/i)).toBeVisible(); }); test("empty-state: a cancelled departure says so, not \"no trains available\"", async ({ page, request }) => { const routeRes = await request.post(`${API_URL}/routes`, { headers: auth(), data: { code: `E2E-EMPTY-CANCELLED-${Date.now()}`, name: "E2E Empty Cancelled Route", effectiveFrom: "2020-01-01T00:00:00Z", stops: [ { stationId: STATIONS.A, sequence: 1, distanceKm: 0 }, { stationId: STATIONS.C, sequence: 2, distanceKm: 100 }, ], }, }); expect(routeRes.ok()).toBeTruthy(); const route = (await routeRes.json())?.data; const templateRes = await request.put(`${API_URL}/routes/${route.id}/coaches`, { headers: auth(), data: { coaches: [{ coachId: UI_IDS.coach, positionNumber: 1 }] }, }); expect(templateRes.ok()).toBeTruthy(); const date = daysFromNowDate(60); const dep = new Date(`${date}T06:00:00Z`); const arr = new Date(dep.getTime() + 4 * 3600_000); const scheduleRes = await request.post(`${API_URL}/schedules`, { headers: auth(), data: { trainId: UI_IDS.train, routeId: route.id, departureAt: dep.toISOString(), arrivalAt: arr.toISOString() }, }); expect(scheduleRes.ok()).toBeTruthy(); const schedule = (await scheduleRes.json())?.data; const cancelRes = await request.patch(`${API_URL}/schedules/${schedule.id}/status`, { headers: auth(), data: { status: "CANCELLED" }, }); expect(cancelRes.ok()).toBeTruthy(); await page.goto(customResultsUrl(STATIONS.A, STATIONS.C, date), { waitUntil: "domcontentloaded" }); await expect(page.getByText("Departure cancelled")).toBeVisible({ timeout: 20_000 }); await expect(page.getByText(/was cancelled/i)).toBeVisible(); }); test("empty-state: an unconnected station pair says EDR doesn't run there, with no date picker", async ({ page, request, }) => { const stationRes = await request.post(`${API_URL}/stations`, { headers: auth(), data: { code: `E2E-ISO-${Date.now() % 100000}`, name: "E2E Isolated Station", city: "Nowhere" }, }); expect(stationRes.ok()).toBeTruthy(); const isolatedStation = (await stationRes.json())?.data; await page.goto(customResultsUrl(STATIONS.A, isolatedStation.id, sampleDepartDate()), { waitUntil: "domcontentloaded", }); await expect(page.getByText("Route not available")).toBeVisible({ timeout: 20_000 }); await expect(page.getByText(/doesn't currently run trains between/i)).toBeVisible(); // NO_ROUTE is not fixable by picking another date — no calendar/"Change Date" affordance. await expect(page.getByText(/change date/i)).toHaveCount(0); await expect(page.getByRole("button", { name: /modify search/i })).toBeVisible(); });