import { expect, test, type APIRequestContext } from "@playwright/test"; async function resetDemoData(request: APIRequestContext) { await request.post("/api/v1/demo/login", { data: { role: "operations_manager" } }); await request.post("/api/v1/demo/reset"); } // Verifies the "stretched link" pattern used across the Attention Queue, Today's movements, // Vehicles, Bookings and Data Quality tables: the whole row is one activation target, not // just its title/reference text, while any secondary in-row link stays independently usable. test.beforeEach(async ({ page, request }) => { await resetDemoData(request); await page.addInitScript(() => localStorage.setItem("fleetops.language", "en-GB")); await page.goto("/login"); await page.getByRole("button", { name: "Explore as Operations Manager" }).click(); await expect(page).toHaveURL(/\/dashboard$/); }); test("attention queue row opens its record when clicking empty row space, not just the title", async ({ page }) => { const row = page.locator(".attention-list li.row-clickable").first(); await expect(row).toBeVisible(); const box = await row.boundingBox(); expect(box).not.toBeNull(); // Click near the far right edge of the row -- empty space, not the title text or badge. await page.mouse.click(box!.x + box!.width - 10, box!.y + box!.height / 2); await expect(page).toHaveURL(/\/(data-quality|vehicles)\//); }); test("attention queue row is keyboard reachable and opens on Enter", async ({ page }) => { const row = page.locator(".attention-list li.row-clickable").first(); const link = row.locator(".row-link"); await link.focus(); await expect(link).toBeFocused(); await page.keyboard.press("Enter"); await expect(page).toHaveURL(/\/(data-quality|vehicles)\//); }); test("today's movements row opens the correct booking on click", async ({ page }) => { const row = page.locator(".movement-timeline li.row-clickable").first(); await expect(row).toBeVisible(); const box = await row.boundingBox(); await page.mouse.click(box!.x + box!.width - 10, box!.y + box!.height / 2); await expect(page).toHaveURL(/\/bookings\/BK-/); }); test("vehicles table row opens the vehicle detail from empty row space", async ({ page }) => { await page.goto("/vehicles"); const row = page.locator(".data-table tbody tr.row-clickable").first(); await expect(row).toBeVisible(); const ref = (await row.locator("th").first().innerText()).split("\n")[0].trim(); const box = await row.boundingBox(); await page.mouse.click(box!.x + box!.width - 10, box!.y + box!.height / 2); await expect(page).toHaveURL(new RegExp(`/vehicles/${ref}$`)); }); test("bookings table row opens the booking, and the secondary vehicle link stays independently clickable", async ({ page }) => { await page.goto("/bookings"); const row = page.locator(".data-table tbody tr.row-clickable").first(); await expect(row).toBeVisible(); const vehicleLink = row.locator(".cell-link"); const vehicleRef = (await vehicleLink.innerText()).trim(); // Clicking the secondary vehicle-ref link navigates to the vehicle, not the booking -- // it must not be swallowed by the row-spanning overlay link sitting behind it. await vehicleLink.click(); await expect(page).toHaveURL(new RegExp(`/vehicles/${vehicleRef}$`)); await page.goto("/bookings"); const rowAgain = page.locator(".data-table tbody tr.row-clickable").first(); const bookingRef = (await rowAgain.locator("th").first().innerText()).split("\n")[0].trim(); const box = await rowAgain.boundingBox(); await page.mouse.click(box!.x + box!.width - 10, box!.y + box!.height / 2); await expect(page).toHaveURL(new RegExp(`/bookings/${bookingRef}$`)); }); test("data quality table row opens the issue detail from empty row space", async ({ page }) => { await page.goto("/data-quality"); const row = page.locator(".data-table tbody tr.row-clickable").first(); await expect(row).toBeVisible(); const ref = (await row.locator("th").first().innerText()).split("\n")[0].trim(); const box = await row.boundingBox(); await page.mouse.click(box!.x + box!.width - 10, box!.y + box!.height / 2); await expect(page).toHaveURL(new RegExp(`/data-quality/${ref}$`)); }); test("attention queue row opens the correct record on a mobile viewport tap", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await page.goto("/dashboard"); const row = page.locator(".attention-list li.row-clickable").first(); await expect(row).toBeVisible(); const box = await row.boundingBox(); // A real touch context needs `hasTouch`, which this shared spec file doesn't opt into; // a mouse click at the same mobile viewport size still exercises the same CSS layout // and click-target logic, since the app has no touch-specific event handling. await page.mouse.click(box!.x + box!.width - 10, box!.y + box!.height / 2); await expect(page).toHaveURL(/\/(data-quality|vehicles)\//); }); test("data quality table row has a pointer cursor and a visible focus ring covering the whole row", async ({ page }) => { await page.goto("/data-quality"); const row = page.locator(".data-table tbody tr.row-clickable").first(); await expect(row).toHaveCSS("cursor", "pointer"); await row.locator(".row-link").focus(); await expect(row.locator(".row-link")).toBeFocused(); });