import { expect, test } from "@playwright/test"; /** * Create-reservation dialog on the reservations list page. * * NOTE: these tests exercise the real dev server and are run in the final * verification wave (F3) under the dev-server protocol, not on every change. * The dialog is server-gated: only users holding operation-reservations:create * render the "New Reservation" trigger; logistics qualification gates the * vehicle/cargo/budget sections inside. */ test.describe("Create reservation dialog", () => { test("guests never see the create-reservation dialog", async ({ page }) => { await page.goto("/operations/reservations"); await expect(page.getByRole("heading", { level: 1 })).toHaveText("Polaris Task Force"); }); test("a permitted, logistics-qualified user sees the create dialog with all sections", async ({ page, }) => { await page.goto("/login"); await page.getByLabel("Username").fill("dev"); await page.getByLabel("Password").fill("Test123"); await page.getByRole("button", { name: "Log in" }).click(); await page.waitForURL((url) => !url.pathname.startsWith("/login"), { timeout: 30_000 }); await page.goto("/operations/reservations"); await expect(page.getByRole("heading", { name: "Reservations", level: 1 })).toBeVisible(); // The trigger renders for a user holding operation-reservations:create. await expect(page.getByRole("button", { name: "New Reservation" })).toBeVisible(); await page.getByRole("button", { name: "New Reservation" }).click(); // Hydration race: a pre-hydration click is swallowed, and once the dialog // opens the trigger goes aria-hidden behind the overlay, which strands // Playwright's click retries. Force the dispatch and verify the dialog // opened, retrying briefly if the first dispatch landed too early. const trigger = page.getByRole("button", { name: "New Reservation" }); const dialog = page.getByRole("dialog"); for (let attempt = 0; attempt < 5; attempt += 1) { if (await dialog.isVisible().catch(() => false)) break; await trigger.click({ force: true }).catch(() => {}); await page.waitForTimeout(750); } // Core fields are present. await expect(page.getByLabel("Reservation key")).toBeVisible(); await expect(page.getByLabel("Operation id")).toBeVisible(); // Logistics-qualified users get an enabled Vehicles + Cargo + Budget section. await expect(page.getByText("Vehicles (optional)")).toBeVisible(); await expect(page.getByText("Cargo lines")).toBeVisible(); await expect(page.getByText("Budget account (optional)")).toBeVisible(); }); test("submitting the dialog with the required fields missing shows the client error", async ({ page, }) => { await page.goto("/login"); await page.getByLabel("Username").fill("dev"); await page.getByLabel("Password").fill("Test123"); await page.getByRole("button", { name: "Log in" }).click(); await page.waitForURL((url) => !url.pathname.startsWith("/login"), { timeout: 30_000 }); await page.goto("/operations/reservations"); await page.getByRole("button", { name: "New Reservation" }).click(); // Empty submit -> client pre-validation surfaces the required-field errors. await page.getByRole("button", { name: "Reserve assets" }).click(); await expect( page.getByRole("alert").filter({ hasText: "A reservation key is required." }), ).toBeVisible(); }); test("the list renders and either a reserved row or the empty state is present", async ({ page, }) => { await page.goto("/login"); await page.getByLabel("Username").fill("dev"); await page.getByLabel("Password").fill("Test123"); await page.getByRole("button", { name: "Log in" }).click(); await page.waitForURL((url) => !url.pathname.startsWith("/login"), { timeout: 30_000 }); await page.goto("/operations/reservations"); // Either a reserved row (with its Cancel control) or the empty state renders. await expect(page.getByText("No reservations match this view.")).toBeVisible(); }); });