The suite could not pass on the test database: global setup now seeds the shared dev/Test123 login with a superuser Roles doc and the dashboard rank, and points its own Payload connection at the test database. Playwright runs one worker with generous test and expect timeouts because first navigation to a route triggers a cold webpack compile, and gains E2E_BROWSER_PATH to swap the browser binary when the installed Vivaldi crashes under automation. The landing page gains the missing h1 the guest specs assert, login flows wait for the redirect to land, strict-mode locator collisions get exact or level-scoped matches, the session banner test re-signs the login claims with a short expiry using the runtime secret, and the reservation dialog click forces the dispatch and verifies the dialog opened (the trigger goes aria-hidden behind the modal mid-click, which stranded the retry loop).
86 lines
4 KiB
TypeScript
86 lines
4 KiB
TypeScript
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();
|
|
});
|
|
});
|