import AxeBuilder from "@axe-core/playwright"; import { expect, test } from "@playwright/test"; import { authenticatedStatePath } from "../global-setup"; import { expectAuthenticatedShell } from "./support/v2"; test.describe("platform workflows at supported widths", () => { test.use({ storageState: authenticatedStatePath }); for (const width of [320, 360, 390, 768, 1280]) { test(`search and administration reflow without page overflow at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 850 }); for (const [path, heading] of [ ["/search?q=knowledge", "Zoeken"], ["/admin/jobs", "Achtergrondtaken"], ["/admin/audit", "Beheeraudit"], ["/admin/platform", "Platforminstellingen"] ] as const) { await page.goto(path); await expect(page.getByRole("heading", { name: heading, level: 1 })).toBeVisible(); await expectAuthenticatedShell(page); const overflow = await page.evaluate( () => document.documentElement.scrollWidth - document.documentElement.clientWidth ); expect(overflow, `${path} overflowed at ${width}px`).toBeLessThanOrEqual(1); } }); } test("keyboard search and admin navigation remain accessible", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await page.emulateMedia({ reducedMotion: "reduce" }); await page.goto("/search?q=knowledge"); await expect(page.getByRole("heading", { name: "Zoeken", level: 1 })).toBeVisible(); expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]); await page.getByRole("button", { name: "Opdrachten en zoeken openen" }).click(); await expect(page.getByRole("dialog")).toBeVisible(); await page.keyboard.press("Tab"); expect(await page.evaluate(() => { const dialog = document.querySelector("[role=dialog]"); return dialog?.contains(document.activeElement) ?? false; })).toBeTruthy(); const reducedDuration = await page.getByRole("dialog").evaluate( (dialog) => parseFloat(getComputedStyle(dialog).transitionDuration) ); expect(reducedDuration).toBeLessThanOrEqual(0.01); await page.keyboard.press("Escape"); await expect(page.getByRole("dialog")).toBeHidden(); await page.goto("/admin/jobs"); await expect(page.getByRole("heading", { name: "Achtergrondtaken", level: 1 })).toBeVisible(); expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]); }); test("skip-link focus survives delayed session and lazy-route main replacements", async ({ page }) => { let releaseSession!: () => void; const sessionGate = new Promise((resolve) => { releaseSession = resolve; }); await page.route("**/api/v1/auth/session", async (route) => { await sessionGate; await route.continue(); }); try { await page.goto("/infrastructure"); const loadingMain = page.locator("#main-content"); await expect(loadingMain).toContainText("Sessiestatus wordt gecontroleerd…"); const initialLoadingMain = await loadingMain.elementHandle(); if (!initialLoadingMain) throw new Error("The session-loading main was not rendered."); await page.keyboard.press("Tab"); await expect(page.getByRole("link", { name: "Ga naar de inhoud" })).toBeFocused(); await page.keyboard.press("Enter"); await expect(loadingMain).toBeFocused(); // The held session response lets keyboard activation focus the loading main // first. Resolving it then replaces that main through lazy-route rendering. releaseSession(); await expect.poll(() => initialLoadingMain.evaluate((element) => element.isConnected)).toBe(false); await expect(page.locator("#main-content")).toContainText("Domeinen"); await expect(page.locator("#main-content")).toBeFocused(); } finally { releaseSession(); } }); test("narrow mobile controls stay touchable with a short virtual-keyboard viewport", async ({ page }) => { await page.setViewportSize({ width: 320, height: 430 }); await page.goto("/admin/jobs"); await expect(page.getByRole("heading", { name: "Achtergrondtaken", level: 1 })).toBeVisible(); for (const name of ["Navigatie openen", "Opdrachten en zoeken openen", "Accountmenu"]) { const box = await page.getByRole("button", { name }).boundingBox(); // Browser layout rounds CSS's 44px touch target to fractional device pixels. expect(box?.width ?? 0, `${name} width`).toBeGreaterThanOrEqual(43.9); expect(box?.height ?? 0, `${name} height`).toBeGreaterThanOrEqual(43.9); } await page.getByRole("button", { name: "Navigatie openen" }).click(); await expect(page.getByRole("navigation", { name: "Hoofdnavigatie" })).toBeVisible(); expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]); }); test("server preferences survive a reload and can be restored", async ({ page }) => { await page.goto("/settings"); await expect(page.getByRole("heading", { name: "Instellingen", level: 1 })).toBeVisible(); await expect(page.locator(".preference-sync-status")).toContainText("Voorkeuren gesynchroniseerd."); await page.getByRole("button", { name: "Donker" }).click(); await expect.poll(async () => { const response = await page.request.get("/api/v1/me/preferences"); return (await response.json() as { theme: string }).theme; }).toBe("dark"); await page.reload(); await expect(page.getByRole("button", { name: "Donker" })).toHaveClass(/selected/); await page.getByRole("button", { name: "Systeem" }).click(); await expect.poll(async () => { const response = await page.request.get("/api/v1/me/preferences"); return (await response.json() as { theme: string }).theme; }).toBe("system"); }); test("administrator can register an external source and record an observation", async ({ page }) => { const sourceKey = `e2e-source-${crypto.randomUUID()}`; const displayName = `E2E bron ${sourceKey.slice(-8)}`; await page.goto("/admin/integrations"); await expect(page.getByRole("heading", { name: "Externe gegevensbronnen", level: 1 })).toBeVisible(); await page.getByRole("button", { name: "Bron toevoegen" }).click(); await page.getByRole("textbox", { name: "Unieke broncode" }).fill(sourceKey); await page.getByRole("textbox", { name: "Weergavenaam" }).fill(displayName); await page.getByRole("button", { name: "Bron registreren" }).click(); await expect(page.locator(".form-success")).toContainText("Externe gegevensbron geregistreerd."); const row = page.locator(".data-source-row", { hasText: sourceKey }); await expect(row).toBeVisible(); await row.getByText("Operatorwaarneming registreren").click(); await row.getByRole("spinbutton", { name: "Waargenomen recordaantal" }).fill("42"); await row.getByRole("button", { name: "Waarneming opslaan" }).click(); await expect(page.locator(".form-success")).toContainText(`Operatorwaarneming voor ${displayName} geregistreerd.`); await page.reload(); await expect(page.locator(".data-source-row", { hasText: sourceKey })).toContainText("42 records"); }); test("administrator sees durable backup job progress after queuing", async ({ page }) => { await page.goto("/admin/jobs"); await expect(page.getByRole("heading", { name: "Achtergrondtaken", level: 1 })).toBeVisible(); await page.getByRole("button", { name: "Back-uptaak inplannen" }).click(); await expect(page.locator(".form-success")).toContainText("Back-uptaak is ingepland."); await expect(page).toHaveURL(/jobId=/); await expect(page.getByRole("heading", { name: /Back-up · / })).toBeVisible(); await expect(page.locator(".admin-job-detail").getByRole("progressbar", { name: /Back-up/ })).toBeVisible(); await page.reload(); await expect(page.getByRole("heading", { name: "Back-up · Geslaagd" })).toBeVisible({ timeout: 20_000 }); }); });