Files
MobilityOps/frontend/e2e/demo.spec.ts
T
NuklearRabbit f5212959b4 feat(returns): add authoritative return preview
The return-review step predicted operational consequences independently in
the frontend, and got it wrong: damage or a technical warning was described
as routing to "maintenance" when the actual domain rule (returns.py) routes
it to "blocked", and the no-contradiction case was described as becoming
"available" when the vehicle actually always goes to "cleaning" first
(only reaching "maintenance" if the service threshold was crossed).

Extract the evaluation returns.py already performed inline into a pure
evaluate_return() function with no writes -- resulting status (with an
explanation), odometer regression, would-create-quality-issue,
next-booking-risk -- and share it between a new non-mutating
POST /bookings/{ref}/return-preview endpoint and the existing commit path,
so preview and commit can never drift apart again. The result screen also
now distinguishes local commit success from n8n delivery (still queued/
unconfirmed) instead of implying both succeeded, and links to any created
quality issue for Operations Manager.
2026-08-02 05:33:12 +02:00

99 lines
4.7 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("five-minute demo script end to end", async ({ page, request }) => {
// Reset via a throwaway API session so the UI test starts from the deterministic seed
// regardless of what earlier test runs mutated (S1 return, S2 merge, etc.).
await resetDemoData(request);
await test.step("1. login as Operations Manager", async () => {
await page.goto("/login");
await expect(page.getByText(/Synthetic proof of concept/)).toBeVisible();
await page.getByRole("button", { name: "Open as Operations Manager" }).click();
await expect(page).toHaveURL(/\/dashboard$/);
});
await test.step("2. verify dashboard metrics are loaded", async () => {
await expect(page.getByRole("heading", { name: "Fleet readiness" })).toBeVisible();
const metricValues = page.locator(".metric-cell dd");
await expect(metricValues.first()).toBeVisible();
const values = await metricValues.allTextContents();
expect(values.length).toBeGreaterThan(0);
expect(values.some((v) => Number(v) > 0)).toBeTruthy();
});
await test.step("3. open active demo booking", async () => {
await page.goto("/bookings/BK-DEMO-RETURN");
await expect(page.getByRole("heading", { name: "BK-DEMO-RETURN" })).toBeVisible();
await expect(page.getByText("active", { exact: true })).toBeVisible();
});
await test.step("4. register an odometer-regression return (S1)", async () => {
const vehicleOdometerText = await page
.locator(".detail-grid div", { hasText: "Start odometer" })
.locator("dd")
.textContent();
const startOdometer = parseInt((vehicleOdometerText ?? "0").replace(/\D/g, ""), 10);
const lowReading = Math.max(0, startOdometer - 500);
await page.getByLabel("End odometer (km)").fill(String(lowReading));
await page.getByLabel("Fuel level (%)").fill("55");
await page.getByRole("button", { name: "Review return" }).click();
await expect(page.getByRole("heading", { name: "Review return impact" })).toBeVisible();
await page.getByRole("button", { name: "Confirm return" }).click();
await expect(page.getByRole("heading", { name: "Return registered" })).toBeVisible();
});
await test.step("5. verify quality issue and queued automation event", async () => {
await expect(page.getByText(/DQ-RET-|None created/)).toBeVisible();
await expect(page.getByText(/Queued for delivery \(/)).toBeVisible();
});
await test.step("6. resolve the duplicate customer scenario (S2)", async () => {
await page.goto("/data-quality/DQ-DEMO-DUPLICATE");
await expect(page.getByRole("heading", { name: "Compare and merge" })).toBeVisible();
await page.getByRole("button", { name: /Merge into CUS-0012/ }).click();
await page.getByRole("button", { name: "Yes, merge" }).click();
await expect(page.getByText("resolved", { exact: true })).toBeVisible();
});
await test.step("7. ask the damage question and inspect citations (S6)", async () => {
await page.goto("/knowledge");
await page
.getByPlaceholder(/What must I do when a vehicle returns with damage/)
.fill("What must I do when a vehicle returns with damage?");
await page.getByRole("button", { name: "Ask" }).click();
await expect(page.getByText("Grounded in cited procedures")).toBeVisible();
await expect(page.getByText("Damage handling procedure").first()).toBeVisible();
await expect(page.getByText("Vehicle return procedure").first()).toBeVisible();
});
await test.step("8. inspect audit entries", async () => {
await page.goto("/audit");
await page.getByLabel("Action").fill("return_registered");
await expect(page.locator(".data-table tbody tr").first()).toBeVisible();
await expect(page.getByText("return registered").first()).toBeVisible();
});
await test.step("9. verify responsive navigation at mobile width", async () => {
await page.setViewportSize({ width: 360, height: 800 });
await page.goto("/dashboard");
await expect(page.getByText(/Synthetic demo data/).first()).toBeVisible();
await expect(page.getByRole("link", { name: "Overview" }).first()).toBeVisible();
const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth);
const clientWidth = await page.evaluate(() => document.documentElement.clientWidth);
expect(scrollWidth).toBeLessThanOrEqual(clientWidth + 1);
});
});