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>
106 lines
5.2 KiB
TypeScript
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();
|
|
});
|