Files
MobilityOps/frontend/e2e/demo-guide.spec.ts
T
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

164 lines
7.8 KiB
TypeScript

import { expect, test, type APIRequestContext } from "@playwright/test";
async function resetDemoData(request: APIRequestContext) {
const login = await request.post("/api/v1/demo/login", { data: { role: "operations_manager" } });
expect(login.ok()).toBeTruthy();
const reset = await request.post("/api/v1/demo/reset");
expect(reset.ok()).toBeTruthy();
}
test.describe.configure({ mode: "serial" });
test("scenario overview lists all 5 scenarios, ready right after a reset", async ({ page, request }) => {
await resetDemoData(request);
await page.goto("/login");
await page.getByRole("button", { name: "Verken als Operations Manager" }).click();
await expect(page).toHaveURL(/\/dashboard$/);
await page.goto("/scenarios");
await expect(page.getByRole("heading", { name: "Probeer een demonstratiescenario" })).toBeVisible();
const cards = page.locator(".scenario-card");
await expect(cards).toHaveCount(5);
for (const label of ["Klaar voor demo"]) {
await expect(page.getByText(label).first()).toBeVisible();
}
await expect(page.getByText("Niet beschikbaar")).toHaveCount(0);
});
test("starting a scenario navigates to its fixed record", async ({ page }) => {
await page.goto("/login");
await page.getByRole("button", { name: "Verken als Operations Manager" }).click();
await expect(page).toHaveURL(/\/dashboard$/);
await page.goto("/scenarios");
const duplicateCard = page.locator(".scenario-card", { hasText: "dubbele klant" });
await duplicateCard.getByRole("link", { name: "Start scenario" }).click();
await expect(page).toHaveURL(/\/data-quality\/DQ-DEMO-DUPLICATE$/);
});
// The default Playwright viewport (1280x720) falls in the "standard desktop/tablet" tier
// (see useViewportTier.ts): the guide is a floating, non-modal panel that auto-collapses
// to a persistent progress chip the moment the visitor acts on "Ga naar deze stap".
test("demo guide: navigating steps, jumping to a step collapses to a chip, and the chip reopens it", async ({ page }) => {
await page.goto("/login");
await page.getByRole("button", { name: "Start begeleide demo" }).click();
await expect(page.getByRole("dialog", { name: "Gegidste demo" })).toBeVisible();
await expect(page.getByRole("heading", { name: "1. Begrijp de operationele status" })).toBeVisible();
await page.getByRole("button", { name: "Volgende" }).click();
await expect(page.getByRole("heading", { name: "2. Open een boeking die aandacht nodig heeft" })).toBeVisible();
await page.getByRole("button", { name: /6\. Stel een vraag/ }).click();
await expect(page.getByRole("heading", { name: "6. Stel een vraag aan de procedureassistent" })).toBeVisible();
await page.getByRole("button", { name: "Ga naar deze stap" }).click();
await expect(page).toHaveURL(/\/knowledge$/);
// Panel auto-collapses to a persistent chip -- it must never sit over the knowledge
// page's primary "Ask" action after navigation.
await expect(page.getByRole("dialog", { name: "Gegidste demo" })).toBeHidden();
const chip = page.getByRole("button", { name: /Demo-gids · stap 6 van 8/ });
await expect(chip).toBeVisible();
await chip.click();
await expect(page.getByRole("dialog", { name: "Gegidste demo" })).toBeVisible();
await expect(page.getByRole("heading", { name: "6. Stel een vraag aan de procedureassistent" })).toBeVisible();
});
test("the collapsed chip has its own close control, independent of reopening it", async ({ page }) => {
await page.goto("/login");
await page.getByRole("button", { name: "Start begeleide demo" }).click();
await page.getByRole("button", { name: "Ga naar deze stap" }).click();
const chip = page.getByRole("button", { name: /Demo-gids · stap/ });
await expect(chip).toBeVisible();
await page.getByRole("button", { name: "Sluiten" }).click();
await expect(chip).toBeHidden();
await expect(page.getByRole("dialog", { name: "Gegidste demo" })).toBeHidden();
});
test("demo guide progress persists across navigation and the trigger shows it", async ({ page }) => {
await page.goto("/login");
await page.getByRole("button", { name: "Start begeleide demo" }).click();
await page.getByRole("button", { name: "Volgende" }).click();
await page.getByRole("button", { name: "Volgende" }).click();
await page.getByRole("button", { name: "Demo-gids inklappen" }).click();
await expect(page.getByRole("button", { name: /^Demo-gids/ }).first()).toContainText("2/8");
await page.goto("/vehicles");
await page.getByRole("button", { name: /^Demo-gids/ }).first().click();
await expect(page.getByRole("heading", { name: "3. Verwerk een retour" })).toBeVisible();
});
test("demo guide is not shown to a rental employee", async ({ page }) => {
await page.goto("/login");
await page.getByRole("button", { name: "Verken als Rental Employee" }).click();
await expect(page).toHaveURL(/\/dashboard$/);
await expect(page.getByRole("button", { name: /Demo-gids/ })).toHaveCount(0);
});
test("restarting the demo from the guide resets data and returns to login", async ({ page }) => {
await page.goto("/login");
await page.getByRole("button", { name: "Start begeleide demo" }).click();
await expect(page.getByRole("dialog", { name: "Gegidste demo" })).toBeVisible();
await page.getByRole("button", { name: "Demo opnieuw voorbereiden" }).click();
await expect(page).toHaveURL(/\/login$/, { timeout: 10000 });
});
test("wide desktop viewport docks the guide as a rail that never collapses to a chip", async ({ page }) => {
await page.setViewportSize({ width: 1600, height: 1000 });
await page.goto("/login");
await page.getByRole("button", { name: "Start begeleide demo" }).click();
const panel = page.locator(".demo-guide-panel.is-wide");
await expect(panel).toBeVisible();
await page.getByRole("button", { name: "Ga naar deze stap" }).click();
await expect(panel).toBeVisible();
await expect(page.locator(".demo-guide-chip")).toHaveCount(0);
});
test("mobile viewport shows a bottom sheet with collapsed/half/full states and no horizontal overflow", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/login");
await page.getByRole("button", { name: "Start begeleide demo" }).click();
const panel = page.locator(".demo-guide-panel.is-mobile");
await expect(panel).toBeVisible();
await expect(panel).toHaveClass(/sheet-half/);
const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth);
const clientWidth = await page.evaluate(() => document.documentElement.clientWidth);
expect(scrollWidth).toBeLessThanOrEqual(clientWidth + 1);
await page.locator(".demo-guide-sheet-handle").click();
await expect(panel).toHaveClass(/sheet-full/);
await page.locator(".demo-guide-sheet-handle").click();
await expect(panel).toHaveClass(/sheet-collapsed/);
});
test("Escape collapses the standard-tier panel, then closes it", async ({ page }) => {
await page.goto("/login");
await page.getByRole("button", { name: "Start begeleide demo" }).click();
await expect(page.getByRole("dialog", { name: "Gegidste demo" })).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByRole("button", { name: /Demo-gids · stap/ })).toBeVisible();
await expect(page.getByRole("dialog", { name: "Gegidste demo" })).toBeHidden();
await page.keyboard.press("Escape");
await expect(page.getByRole("button", { name: /Demo-gids · stap/ })).toBeHidden();
});
test("going to a step scrolls, focuses and highlights the on-page target", async ({ page }) => {
await page.goto("/login");
await page.getByRole("button", { name: "Start begeleide demo" }).click();
await page.getByRole("button", { name: /6\. Stel een vraag/ }).click();
await page.getByRole("button", { name: "Ga naar deze stap" }).click();
await expect(page).toHaveURL(/\/knowledge$/);
const target = page.locator("#ask-heading");
await expect(target).toBeFocused();
await expect(target).toHaveClass(/demo-guide-highlight/);
});