251 lines
8.7 KiB
TypeScript
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();
|
|
}
|
|
});
|