test(app): cover Batch 1 functional-completion regressions

Add Playwright coverage for the fixes in this batch: vehicle search actually
changes the rendered rows, booking pagination stays within 25 rows and page
2 differs from page 1, session survives a refresh, logout invalidates the
server session, direct navigation without a session redirects to login, and
Rental Employee is blocked from manager-only pages both in the UI (hidden
nav, restricted message) and directly against the API (403).
This commit is contained in:
NuklearRabbit
2026-08-02 04:52:07 +02:00
parent 760f3b6ee2
commit e1f0ad8431
+115 -2
View File
@@ -48,6 +48,28 @@ test("vehicles page: status filter and attention-only checkbox both work", async
expect(attentionCells.every((c) => c.includes("Needs attention"))).toBeTruthy();
});
test("vehicles page: free-text search actually filters the rendered rows", async ({ page }) => {
await page.goto("/vehicles");
await expect(page.locator(".data-table")).toBeVisible();
const totalRows = await page.locator(".data-table tbody tr").count();
expect(totalRows).toBeGreaterThan(1);
const searchBox = page.getByRole("form", { name: "Filter vehicles" }).getByLabel("Search");
await searchBox.fill("MO-001");
await expect(async () => {
const rows = await page.locator(".data-table tbody tr").count();
expect(rows).toBe(1);
}).toPass({ timeout: 5000 });
const refs = await page.locator(".data-table tbody tr th a").allTextContents();
expect(refs).toEqual(["MO-001"]);
await searchBox.fill("");
await expect(async () => {
const rows = await page.locator(".data-table tbody tr").count();
expect(rows).toBe(totalRows);
}).toPass({ timeout: 5000 });
});
test("vehicle detail: all tabs render distinct content", async ({ page }) => {
await page.goto("/vehicles/MO-016");
await expect(page.getByRole("heading", { name: /MO-016/ })).toBeVisible();
@@ -66,6 +88,33 @@ test("bookings page: status filter works", async ({ page }) => {
expect(statuses.every((s) => s.includes("returned"))).toBeTruthy();
});
test("bookings page: pagination renders at most 25 rows and page 2 differs from page 1", async ({
page,
}) => {
await page.goto("/bookings");
await expect(page.locator(".data-table")).toBeVisible();
const page1Count = await page.locator(".data-table tbody tr").count();
expect(page1Count).toBeLessThanOrEqual(25);
const page1Refs = await page.locator(".data-table tbody tr th a").allTextContents();
const nextButton = page.getByRole("button", { name: "Next" });
await expect(nextButton).toBeEnabled();
await nextButton.click();
await expect(async () => {
const page2Refs = await page.locator(".data-table tbody tr th a").allTextContents();
expect(page2Refs.length).toBeGreaterThan(0);
expect(page2Refs).not.toEqual(page1Refs);
}).toPass({ timeout: 5000 });
const page2Count = await page.locator(".data-table tbody tr").count();
expect(page2Count).toBeLessThanOrEqual(25);
const prevButton = page.getByRole("button", { name: "Previous" });
await expect(prevButton).toBeEnabled();
});
test("data quality page: status and rule-type filters work", async ({ page }) => {
await page.goto("/data-quality");
await expect(page.locator(".data-table")).toBeVisible();
@@ -141,14 +190,78 @@ test("switch role button logs out and returns to login", async ({ page }) => {
await expect(page).toHaveURL(/\/login$/);
});
test("rental employee role sees restricted automation page and cannot access reset", async ({
test("session survives a page refresh and restores the correct role", async ({ page }) => {
await page.goto("/vehicles");
await page.reload();
await expect(page).toHaveURL(/\/vehicles$/);
await expect(page.getByText("Operations manager")).toBeVisible();
await expect(page.locator(".data-table")).toBeVisible();
});
test("logout invalidates the server session so a refresh returns to login", async ({ page }) => {
await page.goto("/dashboard");
await page.getByRole("button", { name: "Switch role" }).click();
await expect(page).toHaveURL(/\/login$/);
// Directly re-requesting a protected route after logout must not restore access from a
// stale client cache; the server-side cookie is gone.
await page.goto("/dashboard");
await expect(page).toHaveURL(/\/login$/);
});
test("direct navigation to a protected route without a session redirects to login", async ({
page,
context,
}) => {
await context.clearCookies();
await page.goto("/vehicles");
await expect(page).toHaveURL(/\/login$/);
});
test("rental employee role has a restricted nav and cannot reach manager-only pages", async ({
page,
}) => {
await page.getByRole("button", { name: "Switch role" }).click();
await page.getByRole("button", { name: "Open as Rental Employee" }).click();
await expect(page).toHaveURL(/\/dashboard$/);
await page.getByRole("link", { name: "Integrations" }).click();
// Manager-only nav items are not shown at all, not merely disabled.
await expect(page.getByRole("link", { name: "Data quality" })).toHaveCount(0);
await expect(page.getByRole("link", { name: "Integrations" })).toHaveCount(0);
await expect(page.getByRole("link", { name: "Audit trail" })).toHaveCount(0);
// Direct URL navigation is still blocked server-side and shows the same restricted
// message as a defense-in-depth measure, not just a hidden button.
await page.goto("/automation");
await expect(
page.getByText("Automation delivery status is visible to Operations Managers only."),
).toBeVisible();
await page.goto("/data-quality");
await expect(
page.getByText("Data-quality evidence and resolutions are visible to Operations Managers only."),
).toBeVisible();
await page.goto("/audit");
await expect(page.getByText("Audit history is visible to Operations Managers only.")).toBeVisible();
});
test("rental employee direct API access to manager-only endpoints is rejected", async ({
page,
request,
}) => {
await page.getByRole("button", { name: "Switch role" }).click();
await page.getByRole("button", { name: "Open as Rental Employee" }).click();
await expect(page).toHaveURL(/\/dashboard$/);
const cookies = await page.context().cookies();
const sessionCookie = cookies.find((c) => c.name === "mobilityops_session");
const cookieHeader = sessionCookie ? `${sessionCookie.name}=${sessionCookie.value}` : "";
for (const path of ["/api/v1/data-quality/issues", "/api/v1/audit", "/api/v1/workflows"]) {
const response = await request.get(`http://localhost:8128${path}`, {
headers: { Cookie: cookieHeader },
});
expect(response.status(), path).toBe(403);
}
});