Files
itworx-knowledge-hub-Public/tests/e2e/specs/v2-r3-daily-ux-completeness.spec.ts
T

133 lines
6.5 KiB
TypeScript

import AxeBuilder from "@axe-core/playwright";
import { expect, test, type Page } from "@playwright/test";
import { authenticatedStatePath } from "../global-setup";
import { expectAuthenticatedShell } from "./support/v2";
/**
* V2-R3: infrastructure scale/durable-sync remediation's sibling milestone —
* "Stitch fidelity and daily UX" (docs/v2/V2_REMEDIATION_PLAN.md,
* docs/adr/ADR-027-v2-r3-context-inspector-and-daily-ux-completeness.md). This spec
* is the concrete evidence for the `implementation-fidelity-checklist.md` error/
* degraded-state row (#23): every route's own API-failure path shows a real,
* accessible, retry-capable error state, not just the few routes previously proven
* directly.
*/
test.use({ storageState: authenticatedStatePath });
async function forceApiFailure(page: Page, pathPattern: string | RegExp) {
await page.route(pathPattern, (route) =>
route.fulfill({
status: 500,
contentType: "application/problem+json",
body: JSON.stringify({ title: "Onverwachte serverfout.", status: 500, correlationId: "test-correlation-id" })
})
);
}
const errorStateRoutes: Array<{ path: string; apiPattern: RegExp; retryLabel: string }> = [
{ path: "/infrastructure/services", apiPattern: /\/api\/v1\/infrastructure\/services(\?.*)?$/, retryLabel: "Opnieuw" },
{ path: "/projects", apiPattern: /\/api\/v1\/projects(\?.*)?$/, retryLabel: "Opnieuw" },
{ path: "/runbooks", apiPattern: /\/api\/v1\/runbooks(\?.*)?$/, retryLabel: "Opnieuw" }
];
test.describe("V2-R3 error-state sweep: every listed route's own API failure shows a real, accessible, retryable error", () => {
for (const { path, apiPattern, retryLabel } of errorStateRoutes) {
test(`${path} shows a clear error state with a working retry when its own API call fails`, async ({ page }) => {
await forceApiFailure(page, apiPattern);
await page.goto(path);
await expectAuthenticatedShell(page);
const alert = page.getByRole("alert");
await expect(alert).toBeVisible();
const retryButton = alert.getByRole("button", { name: retryLabel });
await expect(retryButton).toBeVisible();
// Never a raw stack trace, exception message or private filesystem path shown
// to the user (CLAUDE.md security rules) — only the route's own honest,
// generic Dutch failure copy.
const alertText = await alert.textContent();
expect(alertText).not.toMatch(/Exception|StackTrace|System\.|at \w+\.\w+\(/);
const accessibility = await new AxeBuilder({ page }).analyze();
expect(accessibility.violations).toEqual([]);
// The retry actually re-issues the same request: clearing the forced failure
// and clicking retry must recover to the real, working screen, not stay stuck.
await page.unroute(apiPattern);
await retryButton.click();
await expect(page.getByRole("alert")).toHaveCount(0);
});
}
});
test.describe("V2-R3 offline banner: a genuine network-level failure is distinguishable from an ordinary server error", () => {
test("a request that fails at the network level (not just a 5xx) still surfaces a clear, non-raw error message", async ({ page }) => {
await page.route(/\/api\/v1\/infrastructure\/services(\?.*)?$/, (route) => route.abort("connectionrefused"));
await page.goto("/infrastructure/services");
await expectAuthenticatedShell(page);
const alert = page.getByRole("alert");
await expect(alert).toBeVisible();
const alertText = await alert.textContent();
expect(alertText).not.toMatch(/Failed to fetch|NetworkError|TypeError/);
});
});
// implementation-fidelity-checklist.md row #21 ("Empty states"): individually proven
// where directly exercised, but with no single dedicated cross-route sweep. Forcing a
// genuinely empty `{ items: [], nextCursor: null }` response via route interception
// (rather than relying on the shared dev database happening to be empty, which it is
// not after a long session's worth of prior specs seeding real content) makes this
// deterministic and repeatable regardless of what real data already exists.
const emptyStateRoutes: Array<{ path: string; apiPattern: RegExp; heading: string }> = [
{ path: "/projects", apiPattern: /\/api\/v1\/projects(\?.*)?$/, heading: "Nog geen projecten" },
{ path: "/runbooks", apiPattern: /\/api\/v1\/runbooks(\?.*)?$/, heading: "Nog geen runbooks" }
];
test.describe("V2-R3 empty-state sweep: a genuinely empty collection shows honest, accessible copy, not a blank screen", () => {
for (const { path, apiPattern, heading } of emptyStateRoutes) {
test(`${path} shows a clear empty state when there is genuinely nothing to list`, async ({ page }) => {
await page.route(apiPattern, (route) =>
route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items: [], nextCursor: null }) })
);
await page.goto(path);
await expectAuthenticatedShell(page);
await expect(page.getByRole("heading", { name: heading, level: 2 })).toBeVisible();
const accessibility = await new AxeBuilder({ page }).analyze();
expect(accessibility.violations).toEqual([]);
});
}
});
// implementation-fidelity-checklist.md row #22 ("Loading/skeleton states"): the
// mid-load page-has-heading-one axe gap is already a known, accepted, purely
// transient false positive (every scan in this suite waits past it) — what was never
// given a dedicated pass is the skeleton's own markup while it is actually visible.
// Holding the route open (never fulfilling it for the duration of this test) keeps
// the app in its loading state deterministically, long enough to assert on it.
test.describe("V2-R3 loading-state check: the skeleton itself renders sensible, non-empty structure while data is still in flight", () => {
test("/runbooks shows a real, non-blank loading skeleton before its data arrives", async ({ page }) => {
let releaseRoute: (() => void) | undefined;
const held = new Promise<void>((resolve) => {
releaseRoute = resolve;
});
await page.route(/\/api\/v1\/runbooks(\?.*)?$/, async (route) => {
await held;
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items: [], nextCursor: null }) });
});
await page.goto("/runbooks");
await expectAuthenticatedShell(page);
// A real, visible loading indicator — not a blank main area — while the request
// is deliberately held open.
await expect(page.getByRole("status")).toBeVisible();
releaseRoute?.();
await expect(page.getByRole("heading", { name: "Nog geen runbooks", level: 2 })).toBeVisible();
});
});