diff --git a/frontend/e2e/interactive-elements.spec.ts b/frontend/e2e/interactive-elements.spec.ts index f32eb29..6b11d69 100644 --- a/frontend/e2e/interactive-elements.spec.ts +++ b/frontend/e2e/interactive-elements.spec.ts @@ -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); + } });