Adding exact reason why train is not available to users search results

This commit is contained in:
Muluhabt
2026-07-25 12:28:07 +03:00
parent 2a0fd9bda5
commit d175514f85
6 changed files with 485 additions and 71 deletions

View File

@@ -0,0 +1,122 @@
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();
});