Files
MobilityOps/frontend/e2e/context-resilience.spec.ts
T
NuklearRabbit 81e3fd63bd
MobilityOps acceptance / backend (push) Failing after 19s
MobilityOps acceptance / frontend (push) Successful in 25s
MobilityOps acceptance / e2e (push) Skipped
M54: harden operations and demo resilience
2026-08-24 03:31:03 +02:00

251 lines
8.7 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import type { AutomationRun } from "../src/api/types";
const apiErrorBody = JSON.stringify({
error: { code: "TEST_UNAVAILABLE", message: "Injected test failure", correlation_id: "test-correlation" },
});
async function loginAsManager(page: Page) {
await page.goto("/login");
await page.getByRole("button", { name: "Verken als Operationsmanager" }).click();
await expect(page).toHaveURL(/\/dashboard$/);
}
async function failFirstManifestRequest(page: Page) {
let requestCount = 0;
await page.route("**/api/v1/demo/manifest", async (route) => {
requestCount += 1;
if (requestCount === 1) {
await route.fulfill({ status: 503, contentType: "application/json", body: apiErrorBody });
return;
}
await route.continue();
});
}
test("session validation does not wait for a stalled system-status request", async ({ page }) => {
let releaseStatus = () => undefined;
const statusGate = new Promise<void>((resolve) => {
releaseStatus = resolve;
});
let sessionRequested = false;
await page.route("**/api/v1/system/status", async (route) => {
await statusGate;
await route.fulfill({ status: 503, contentType: "application/json", body: apiErrorBody });
});
await page.route("**/api/v1/auth/session", async (route) => {
sessionRequested = true;
await route.fulfill({ status: 401, contentType: "application/json", body: apiErrorBody });
});
try {
await page.goto("/login");
await expect(page.getByRole("button", { name: "Verken als Operationsmanager" })).toBeEnabled({
timeout: 2_000,
});
expect(sessionRequested).toBe(true);
} finally {
releaseStatus();
}
});
test("guided demo stays gated while its manifest loads, then exposes an explicit retry", async ({ page }) => {
let releaseManifest = () => undefined;
const manifestGate = new Promise<void>((resolve) => {
releaseManifest = resolve;
});
let markManifestRequested = () => undefined;
const manifestRequested = new Promise<void>((resolve) => {
markManifestRequested = resolve;
});
let manifestAttempts = 0;
await page.route("**/api/v1/demo/manifest", async (route) => {
manifestAttempts += 1;
if (manifestAttempts === 1) {
markManifestRequested();
await manifestGate;
await route.fulfill({ status: 503, contentType: "application/json", body: apiErrorBody });
return;
}
await route.continue();
});
try {
await page.goto("/login");
await manifestRequested;
const guidedDemo = page.getByRole("button", { name: "Start begeleide demo" });
await expect(guidedDemo).toBeDisabled();
await expect(page.getByText("Begeleide demo voorbereiden…")).toBeVisible();
releaseManifest();
await expect(page.getByText("De begeleide demo-informatie kon niet geladen worden.")).toBeVisible();
await expect(guidedDemo).toBeDisabled();
await page.getByRole("button", { name: "Demo-informatie opnieuw laden" }).click();
await expect(guidedDemo).toBeEnabled();
expect(manifestAttempts).toBeGreaterThanOrEqual(2);
} finally {
releaseManifest();
}
});
test("guided demo does not continue until the manifest reports it ready", async ({ page }) => {
let returnNotReady = true;
await page.route("**/api/v1/demo/manifest", async (route) => {
if (!returnNotReady) {
await route.continue();
return;
}
const response = await route.fetch();
const manifest = await response.json() as Record<string, unknown>;
await route.fulfill({ response, json: { ...manifest, guide_available: false } });
});
await page.goto("/login");
const guidedDemo = page.getByRole("button", { name: "Start begeleide demo" });
await expect(guidedDemo).toBeDisabled();
await expect(page.getByText("De begeleide demo is momenteel niet klaar om te starten.")).toBeVisible();
await guidedDemo.evaluate((button) => {
button.removeAttribute("disabled");
(button as HTMLButtonElement).click();
});
await expect(page).toHaveURL(/\/login$/);
returnNotReady = false;
await page.getByRole("button", { name: "Demo-informatie opnieuw laden" }).click();
await expect(guidedDemo).toBeEnabled();
});
test("blocked sessionStorage cannot break login or logout", async ({ page }) => {
await page.addInitScript(() => {
Object.defineProperties(Storage.prototype, {
setItem: {
configurable: true,
value: () => {
throw new DOMException("Storage is blocked", "SecurityError");
},
},
removeItem: {
configurable: true,
value: () => {
throw new DOMException("Storage is blocked", "SecurityError");
},
},
});
});
await loginAsManager(page);
await expect(page.getByText("Amelie De Ridder").first()).toBeVisible();
await page.locator(".operator-menu > summary").click();
await page.getByRole("button", { name: "Wissel van rol" }).click();
await expect(page).toHaveURL(/\/login$/);
});
test("scenario manifest failure is visible and retryable", async ({ page }) => {
await failFirstManifestRequest(page);
await loginAsManager(page);
await page.locator('a[href="/scenarios"]').first().click();
await expect(page).toHaveURL(/\/scenarios$/);
await expect(page.getByText("De demonstratiescenario's konden niet geladen worden.")).toBeVisible();
await page.getByRole("button", { name: "Opnieuw proberen" }).click();
await expect(page.locator(".scenario-card")).toHaveCount(5);
});
test("engineering manifest failure is visible and retryable", async ({ page }) => {
await failFirstManifestRequest(page);
await loginAsManager(page);
await page.locator('a[href="/about"]').first().click();
await expect(page).toHaveURL(/\/about$/);
await expect(page.getByText("De demo-informatie kon niet geladen worden.")).toBeVisible();
await page.getByRole("button", { name: "Opnieuw proberen" }).click();
await expect(page.getByRole("heading", { name: "Controle, betrouwbaarheid en uitlegbaarheid" })).toBeVisible();
});
test("the latest automation filter wins when an older response completes last", async ({ page }) => {
test.setTimeout(45_000);
let releaseOldResponse = () => undefined;
const oldResponseGate = new Promise<void>((resolve) => {
releaseOldResponse = resolve;
});
let markOldRequestStarted = () => undefined;
const oldRequestStarted = new Promise<void>((resolve) => {
markOldRequestStarted = resolve;
});
let markOldResponseAttempted = () => undefined;
const oldResponseAttempted = new Promise<void>((resolve) => {
markOldResponseAttempted = resolve;
});
const run = (status: "failed" | "pending", aggregateRef: string): AutomationRun => ({
event_id: status === "failed"
? "11111111-1111-4111-8111-111111111111"
: "22222222-2222-4222-8222-222222222222",
event_type: "vehicle.returned.v1",
aggregate_ref: aggregateRef,
status,
attempts: status === "failed" ? 2 : 0,
last_error: null,
last_error_code: null,
is_demo_scenario: false,
occurred_at: "2026-08-23T10:00:00Z",
});
await page.route(/\/api\/v1\/workflows(?:\?.*)?$/, async (route) => {
const requestedStatus = new URL(route.request().url()).searchParams.get("status");
if (requestedStatus === "failed") {
markOldRequestStarted();
await oldResponseGate;
try {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([run("failed", "BK-RACE-OLD")]),
});
} catch {
// Chromium cancels the superseded fetch. The delayed route can therefore be
// closed before Playwright gets to fulfil it; either outcome proves the same
// contract as long as it cannot overwrite the newer response.
} finally {
markOldResponseAttempted();
}
return;
}
if (requestedStatus === "pending") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([run("pending", "BK-RACE-NEW")]),
});
return;
}
await route.continue();
});
try {
await loginAsManager(page);
await page.goto("/automation");
await expect(page.getByRole("heading", { name: "Automatiseringsopdrachten" })).toBeVisible();
const statusFilter = page.getByRole("combobox", { name: "Status", exact: true });
await statusFilter.selectOption("failed");
await oldRequestStarted;
await statusFilter.selectOption("pending");
await expect(page.getByText("BK-RACE-NEW", { exact: true })).toBeVisible();
releaseOldResponse();
await oldResponseAttempted;
await expect(page.getByText("BK-RACE-NEW", { exact: true })).toBeVisible();
await expect(page.getByText("BK-RACE-OLD", { exact: true })).toHaveCount(0);
} finally {
releaseOldResponse();
}
});