Files
MobilityOps/frontend/e2e/clickable-rows.spec.ts
NuklearRabbitandClaude Sonnet 5 337f8716bb polish: rebrand to Fleet Ops, add trilingual i18n, adaptive demo guide, and UX overhaul
Rebrands the product from MobilityOps to Fleet Ops across the UI, backend defaults and
knowledge base, and makes nl-BE/en-GB/fr-BE full first-class languages: i18next with
eager-bundled per-namespace resources, a persisted accessible language switcher (topbar
and mobile drawer), locale-aware date/number formatting, and a coverage test that fails
the build on any missing or empty translation key.

Backend dynamic content (demo scenarios, blocked-reason text, integration status) moves
from fixed English/Dutch prose to stable message codes + params so the frontend can
localize it; the demo knowledge base gains a fully translated NL/EN/FR procedure corpus
(11 documents each) with per-language retrieval and localized evidence-state messages.

The Demo Guide becomes breakpoint-adaptive: a docked rail on extra-wide desktop, a
floating panel that auto-collapses to a persistent, closable progress chip on standard
desktop/tablet, and a collapsed/half/full bottom sheet on mobile -- with scroll+focus+
highlight on "go to this step", Escape handling, and reduced-motion support.

The Data Quality Workbench gets accessible choice-card decisions with a clear primary/
secondary/tertiary action hierarchy; the Automation ledger groups repeated successes and
uses meaningful short refs; the Audit trail groups events by correlation id with human
action labels and readable before/after diffs. Attention Queue, Today's movements,
Vehicles, Bookings and Data Quality rows are fully clickable (stretched-link pattern)
with independent secondary links, keyboard support and mobile touch targets.

Fixes a topbar overflow on mobile caused by the new language switcher (moved into the
mobile drawer at <=960px) and two dangling aria-labelledby references introduced this
session. Updates all affected Playwright specs for the new nl-BE default and the new
Audit/DemoGuide DOM structure, and adds new i18n-coverage, demo-guide-adaptive and
clickable-rows specs. 131 backend tests, Ruff and mypy, and 71 Playwright tests pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 18:33:22 +02:00

106 lines
5.2 KiB
TypeScript

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();
});