import { expect, test, type APIRequestContext, type Page } from "@playwright/test"; // Targeted end-to-end coverage for the Fleet Ops correction brief (docs/fleet-ops- // correction/): branding, the redesigned status-recommendation flow (preview/apply/ // manual-review/stale-token), MO-016 order independence, trilingual knowledge // grounding, and localized audit/automation content. See also i18n-coverage.spec.ts // (key parity, brand invariant, translation-quality) and responsive-i18n.spec.ts // (breakpoint matrix) for the complementary static-content checks. 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(); // /api/v1/demo/reset deletes the session cookie (it recreates the users table), so any // further authenticated call through this same request context needs a fresh login. const relogin = await request.post("/api/v1/demo/login", { data: { role: "operations_manager" } }); expect(relogin.ok()).toBeTruthy(); } const EXPLORE_OPS_MANAGER: Record = { "nl-BE": "Verken als Operations Manager", "en-GB": "Explore as Operations Manager", "fr-BE": "Explorer en tant qu'Operations Manager", }; const REVIEW_RECOMMENDATION: Record = { "nl-BE": "Aanbeveling bekijken", "en-GB": "Review recommendation", "fr-BE": "Voir la recommandation", }; const CHANGE_STATUS_PREFIX: Record = { "nl-BE": /^Status wijzigen naar/, "en-GB": /^Change status to/, "fr-BE": /^Changer le statut vers/, }; const MANUAL_REVIEW_HEADING: Record = { "nl-BE": "Handmatige beoordeling vereist", "en-GB": "Manual review required", "fr-BE": "Évaluation manuelle requise", }; async function loginAsOpsManager(page: Page, lang: string) { await page.addInitScript((l) => localStorage.setItem("fleetops.language", l), lang); await page.goto("/login"); await page.getByRole("button", { name: EXPLORE_OPS_MANAGER[lang] }).click(); await expect(page).toHaveURL(/\/dashboard$/); } test.describe.configure({ mode: "serial" }); test.describe("branding", () => { for (const lang of ["nl-BE", "en-GB", "fr-BE"]) { test(`Fleet Ops is the visible brand and no MobilityOps/PoC leaks through (${lang})`, async ({ page, request, }) => { await resetDemoData(request); await loginAsOpsManager(page, lang); await expect(page.locator(".brand-mark").first()).toBeVisible(); await expect(page.getByText("Fleet Ops", { exact: true }).first()).toBeVisible(); await expect(page.locator(".app-footer")).toContainText("Fleet Ops"); await expect(page.locator("html")).toHaveAttribute("lang", lang); for (const path of ["/dashboard", "/vehicles", "/data-quality", "/audit", "/automation", "/knowledge"]) { await page.goto(path); const text = await page.locator("body").innerText(); expect(text, `${path} (${lang})`).not.toContain("MobilityOps"); expect(text, `${path} (${lang})`).not.toMatch(/\bPoC\b/); } }); } }); test("language switcher control changes the UI and persists across a reload", async ({ page, request }) => { await resetDemoData(request); // Deliberately not using loginAsOpsManager here: its addInitScript would re-force // nl-BE on every reload, defeating exactly the persistence behaviour under test. await page.goto("/login"); await page.getByRole("button", { name: EXPLORE_OPS_MANAGER["nl-BE"] }).click(); await expect(page).toHaveURL(/\/dashboard$/); await expect(page.getByRole("heading", { name: "Aandachtspunten" })).toBeVisible(); await page.getByRole("combobox", { name: "Taal" }).selectOption("fr-BE"); await expect(page.getByRole("heading", { name: "File d'attention" })).toBeVisible(); await expect(page.locator("html")).toHaveAttribute("lang", "fr-BE"); await page.reload(); await expect(page.getByRole("heading", { name: "File d'attention" })).toBeVisible(); await expect(page.locator("html")).toHaveAttribute("lang", "fr-BE"); }); test.describe("status-recommendation flow", () => { test("preview does not mutate anything, apply names the exact target status", async ({ page, request }) => { await resetDemoData(request); await loginAsOpsManager(page, "nl-BE"); await page.goto("/data-quality/DQ-DEMO-STATUS"); await expect(page.getByRole("heading", { name: "DQ-DEMO-STATUS" })).toBeVisible(); await page.getByRole("button", { name: REVIEW_RECOMMENDATION["nl-BE"] }).click(); await expect(page.getByText("Aanbevolen status")).toBeVisible(); await expect(page.getByRole("heading", { name: "Waarom" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Gevolg" })).toBeVisible(); // Previewing must not have resolved the issue -- still open, using the page's own // authenticated session (page.request shares cookies with the browser context). const issue = await page.request.get("/api/v1/data-quality/issues/DQ-DEMO-STATUS"); expect((await issue.json()).status).toBe("open"); const confirmButton = page.getByRole("button", { name: CHANGE_STATUS_PREFIX["nl-BE"] }); await expect(confirmButton).toHaveText(/Geblokkeerd/); await confirmButton.click(); await expect(page.getByText("Toegepast", { exact: false })).toBeVisible(); const resolved = await page.request.get("/api/v1/data-quality/issues/DQ-DEMO-STATUS"); expect((await resolved.json()).status).toBe("resolved"); }); test("manual review state offers no generic apply button for a genuine fact contradiction", async ({ page, request, }) => { await resetDemoData(request); const issues = await ( await request.get("/api/v1/data-quality/issues", { params: { rule_type: "vehicle_status_conflict", status: "open" }, }) ).json(); const conflicted = issues.find((i: { entity_ref: string }) => i.entity_ref === "MO-024"); expect(conflicted, "expected MO-024's vehicle_status_conflict issue to exist after reset").toBeTruthy(); await loginAsOpsManager(page, "en-GB"); await page.goto(`/data-quality/${conflicted.public_ref}`); await page.getByRole("button", { name: REVIEW_RECOMMENDATION["en-GB"] }).click(); await expect(page.getByRole("heading", { name: MANUAL_REVIEW_HEADING["en-GB"] })).toBeVisible(); await expect(page.getByRole("button", { name: /^Change status to/ })).toHaveCount(0); }); test("a stale recommendation is rejected and the user must review again before applying", async ({ page, request, }) => { await resetDemoData(request); await loginAsOpsManager(page, "en-GB"); await page.goto("/data-quality/DQ-DEMO-STATUS"); await page.getByRole("button", { name: REVIEW_RECOMMENDATION["en-GB"] }).click(); await expect(page.getByRole("button", { name: /^Change status to/ })).toBeVisible(); // Simulate the underlying facts changing after the preview was shown (the same // session resolves the booking overlap in the meantime) -- the previously-fetched // recommendation token must no longer be accepted. await page.evaluate(async () => { await fetch("/api/v1/data-quality/issues/DQ-DEMO-OVERLAP/resolve-overlap", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ booking_ref: "BK-DEMO-OVERLAP-B" }), }); }); await page.getByRole("button", { name: /^Change status to/ }).click(); await expect(page.getByText(/situation has changed/i)).toBeVisible(); await expect(page.getByRole("button", { name: REVIEW_RECOMMENDATION["en-GB"] })).toBeVisible(); }); }); test.describe("MO-016 status conflict is order-independent", () => { // Order independence does NOT mean "the same final vehicle status regardless of // order" -- resolving the booking overlap first genuinely removes the conflict, so // there is correctly nothing left to apply afterwards. What must hold in either // order: the recommendation always reflects the real, current facts (never a stale // "was some other issue open" proxy), and nothing unsafe is ever applied (never // "rented"). test("resolving the booking overlap first correctly leaves nothing to apply", async ({ page, request }) => { await resetDemoData(request); await loginAsOpsManager(page, "nl-BE"); await page.goto("/data-quality/DQ-DEMO-OVERLAP"); await page.getByRole("radio", { name: /BK-DEMO-OVERLAP-B blokkeren/ }).check(); await page.getByRole("button", { name: /BK-DEMO-OVERLAP-B blokkeren/ }).click(); await expect(page.getByText("Opgelost").first()).toBeVisible(); await page.goto("/data-quality/DQ-DEMO-STATUS"); await page.getByRole("button", { name: REVIEW_RECOMMENDATION["nl-BE"] }).click(); await expect(page.getByRole("heading", { name: "Geen wijziging nodig" })).toBeVisible(); await expect(page.getByRole("button", { name: CHANGE_STATUS_PREFIX["nl-BE"] })).toHaveCount(0); const vehicle = await request.get("/api/v1/vehicles/MO-016"); expect((await vehicle.json()).operational_status).toBe("available"); }); test("resolving the status conflict first safely blocks the vehicle, unaffected by the later overlap fix", async ({ page, request, }) => { await resetDemoData(request); await loginAsOpsManager(page, "nl-BE"); await page.goto("/data-quality/DQ-DEMO-STATUS"); await page.getByRole("button", { name: REVIEW_RECOMMENDATION["nl-BE"] }).click(); await page.getByRole("button", { name: CHANGE_STATUS_PREFIX["nl-BE"] }).click(); await expect(page.getByText("Toegepast", { exact: false })).toBeVisible(); const vehicleMid = await request.get("/api/v1/vehicles/MO-016"); const statusAfterApply = (await vehicleMid.json()).operational_status; expect(statusAfterApply).not.toBe("rented"); await page.goto("/data-quality/DQ-DEMO-OVERLAP"); await page.getByRole("radio", { name: /BK-DEMO-OVERLAP-B blokkeren/ }).check(); await page.getByRole("button", { name: /BK-DEMO-OVERLAP-B blokkeren/ }).click(); await expect(page.getByText("Opgelost").first()).toBeVisible(); // Resolving the now-redundant overlap afterwards must not itself change the // vehicle's status as a side effect. const vehicleFinal = await request.get("/api/v1/vehicles/MO-016"); const statusFinal = (await vehicleFinal.json()).operational_status; expect(statusFinal).toBe(statusAfterApply); expect(statusFinal).not.toBe("rented"); await resetDemoData(request); }); }); test.describe("knowledge base is grounded in the operator's own language", () => { const cases: { lang: string; question: string; sourceHint: RegExp }[] = [ { lang: "nl-BE", question: "Wat moet ik doen wanneer een voertuig beschadigd terugkomt?", sourceHint: /schadeafhandeling/i, }, { lang: "en-GB", question: "What should I do when a vehicle returns with damage?", sourceHint: /damage/i, }, { lang: "fr-BE", question: "Que dois-je faire lorsqu'un véhicule revient endommagé ?", sourceHint: /dommages/i, }, ]; for (const { lang, question, sourceHint } of cases) { test(`grounded ${lang} answer cites a ${lang} source about damage`, async ({ page, request }) => { await resetDemoData(request); await loginAsOpsManager(page, lang); await page.goto("/knowledge"); await page.locator("#knowledge-question").fill(question); await page.getByRole("button", { name: /^(Vraag stellen|Ask|Demander)$/ }).click(); await expect(page.getByText(sourceHint).first()).toBeVisible({ timeout: 10_000 }); }); } }); test("audit trail shows localized action and field labels with raw codes only in technical details", async ({ page, request, }) => { await resetDemoData(request); await loginAsOpsManager(page, "nl-BE"); await page.goto("/data-quality/DQ-DEMO-STATUS"); await page.getByRole("button", { name: REVIEW_RECOMMENDATION["nl-BE"] }).click(); await page.getByRole("button", { name: CHANGE_STATUS_PREFIX["nl-BE"] }).click(); await expect(page.getByText("Toegepast", { exact: false })).toBeVisible(); await page.goto("/audit"); // Applying resolves both the vehicle status and the issue in one correlated action -- // expand the group's "technical events" toggle so the other audit row's diff renders // too, regardless of which one the grouping picked as primary. const toggle = page.getByRole("button", { name: /technische gebeurtenis/ }).first(); await expect(toggle).toBeVisible(); await toggle.click(); await expect(page.getByRole("button", { name: "Technische gebeurtenissen verbergen" })).toBeVisible(); await expect(page.getByText("Aanbevolen status toegepast").first()).toBeVisible(); const diffs = page.locator(".change-diff"); await expect(diffs.first()).toBeVisible(); const combinedDiffText = (await diffs.allInnerTexts()).join(" "); expect(combinedDiffText).toContain("Operationele status"); expect(combinedDiffText).not.toContain("operational_status"); }); test("automation shows a localized error explanation with the raw error only under technical details", async ({ page, request, }) => { await resetDemoData(request); await loginAsOpsManager(page, "nl-BE"); await page.goto("/automation"); await expect(page.getByText(/tijdelijk niet bereikbaar/).first()).toBeVisible(); await expect(page.getByText("Synthetic connection timeout to n8n")).not.toBeVisible(); await page.getByText("Technische details").first().click(); await expect(page.getByText("Synthetic connection timeout to n8n")).toBeVisible(); });