mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
137 lines
5.9 KiB
TypeScript
137 lines
5.9 KiB
TypeScript
import { test, expect, type Page } from "@playwright/test";
|
|
import { API_URL, STATIONS, staffToken } from "../../fixtures/data";
|
|
|
|
/**
|
|
* Search form — date selectability is driven by whether a route actually exists.
|
|
*
|
|
* Two behaviours, both backed by GET /search/available-dates:
|
|
*
|
|
* 1. No route between the selected From/To → the Date control is DISABLED outright and an
|
|
* explanation is shown. Letting someone pick a date and only failing at submit wastes the
|
|
* interaction, and graying out individual days would imply the date is the problem when
|
|
* the station pair is.
|
|
* 2. A route exists → the control is enabled, and only the days with no bookable schedule
|
|
* are individually disabled inside the calendar.
|
|
*
|
|
* The form prefills From/To from the URL query (see booking/search/page.tsx), so these specs set
|
|
* the pair deterministically instead of driving the station modal across three layout variants.
|
|
*
|
|
* Runs in the `guest` project — the search form needs no authentication.
|
|
*/
|
|
|
|
/** A station on no route at all — created per-run so the shared seed stays untouched. */
|
|
async function createOrphanStation(request: Page["request"]): Promise<string> {
|
|
const res = await request.post(`${API_URL}/stations`, {
|
|
headers: { Authorization: `Bearer ${staffToken()}` },
|
|
data: {
|
|
code: `ORP${Date.now().toString().slice(-4)}`,
|
|
name: `Orphan ${Date.now()}`,
|
|
city: "Nowhere",
|
|
},
|
|
});
|
|
expect(res.ok(), `station create failed: ${res.status()} ${await res.text()}`).toBeTruthy();
|
|
const body = await res.json();
|
|
return body?.data?.id ?? body?.id;
|
|
}
|
|
|
|
function searchUrl(origin: string, destination: string): string {
|
|
return `/booking/search?origin=${origin}&destination=${destination}`;
|
|
}
|
|
|
|
/** The departure-date trigger. Several layout variants exist; only one is visible at a time. */
|
|
function dateTrigger(page: Page) {
|
|
return page
|
|
.locator("button")
|
|
.filter({ hasText: /^(Departure date|Departure|Select date|Date)$/i })
|
|
.first();
|
|
}
|
|
|
|
test.describe("search date availability", () => {
|
|
test("no route between the stations disables the date picker", async ({ page, request }) => {
|
|
const orphanId = await createOrphanStation(request);
|
|
|
|
await page.goto(searchUrl(STATIONS.A, orphanId), { waitUntil: "domcontentloaded" });
|
|
|
|
// The notice only renders once /search/available-dates has answered routeExists:false.
|
|
await expect(page.getByTestId("no-route-notice").first()).toBeVisible({ timeout: 20_000 });
|
|
await expect(dateTrigger(page)).toBeDisabled();
|
|
});
|
|
|
|
test("a disabled date picker cannot be opened", async ({ page, request }) => {
|
|
const orphanId = await createOrphanStation(request);
|
|
|
|
await page.goto(searchUrl(STATIONS.A, orphanId), { waitUntil: "domcontentloaded" });
|
|
await expect(page.getByTestId("no-route-notice").first()).toBeVisible({ timeout: 20_000 });
|
|
|
|
// force:true bypasses Playwright's own actionability guard, so this asserts the app
|
|
// really refuses to open — not merely that the button looks unclickable.
|
|
await dateTrigger(page).click({ force: true }).catch(() => {});
|
|
await page.waitForTimeout(500);
|
|
await expect(page.getByRole("button", { name: /^\d{1,2}$/ })).toHaveCount(0);
|
|
});
|
|
|
|
test("a route that exists leaves the date picker enabled and openable", async ({ page }) => {
|
|
await page.goto(searchUrl(STATIONS.A, STATIONS.C), { waitUntil: "domcontentloaded" });
|
|
|
|
// Give the availability query the same chance to resolve as the no-route case gets.
|
|
await page.waitForTimeout(3_000);
|
|
await expect(page.getByTestId("no-route-notice")).toHaveCount(0);
|
|
|
|
const trigger = dateTrigger(page);
|
|
await expect(trigger).toBeEnabled();
|
|
await trigger.click();
|
|
// Calendar day cells confirm it actually opened.
|
|
await expect(page.getByRole("button", { name: /^\d{1,2}$/ }).first()).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
});
|
|
|
|
test("dates with no bookable schedule are individually disabled", async ({ page }) => {
|
|
await page.goto(searchUrl(STATIONS.A, STATIONS.C), { waitUntil: "domcontentloaded" });
|
|
await page.waitForTimeout(3_000);
|
|
|
|
await dateTrigger(page).click();
|
|
const dayCells = page.getByRole("button", { name: /^\d{1,2}$/ });
|
|
await expect(dayCells.first()).toBeVisible({ timeout: 10_000 });
|
|
|
|
// The seed creates exactly one bookable departure in the window, so the calendar must
|
|
// contain both enabled and disabled days — never all-enabled (which would mean the
|
|
// availability data was ignored) and never all-disabled (which would mean it was
|
|
// misapplied to a route that does have a trip).
|
|
const total = await dayCells.count();
|
|
let disabled = 0;
|
|
for (let i = 0; i < total; i++) {
|
|
if (await dayCells.nth(i).isDisabled()) disabled++;
|
|
}
|
|
expect(total).toBeGreaterThan(0);
|
|
expect(disabled).toBeGreaterThan(0);
|
|
expect(disabled).toBeLessThan(total);
|
|
});
|
|
|
|
test("switching to a routeless destination clears an already-chosen date", async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
const orphanId = await createOrphanStation(request);
|
|
|
|
// Start on a valid pair with a date already in the URL, so a value is definitely set.
|
|
const withDate = new Date();
|
|
withDate.setDate(withDate.getDate() + 2);
|
|
const dateStr = withDate.toISOString().slice(0, 10);
|
|
await page.goto(`${searchUrl(STATIONS.A, STATIONS.C)}&date=${dateStr}`, {
|
|
waitUntil: "domcontentloaded",
|
|
});
|
|
await page.waitForTimeout(2_500);
|
|
|
|
// Now navigate to the routeless pair — the stale date must not survive behind the
|
|
// disabled control, or a doomed search could still be submitted.
|
|
await page.goto(searchUrl(STATIONS.A, orphanId), { waitUntil: "domcontentloaded" });
|
|
await expect(page.getByTestId("no-route-notice").first()).toBeVisible({ timeout: 20_000 });
|
|
|
|
const trigger = dateTrigger(page);
|
|
await expect(trigger).toBeDisabled();
|
|
// Placeholder text (not a formatted date) proves the value was cleared.
|
|
await expect(trigger).not.toContainText(/\d{4}/);
|
|
});
|
|
});
|