Files
itworx-knowledge-hub-Public/tests/e2e/specs/platform-productization.spec.ts
T

152 lines
7.9 KiB
TypeScript

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<void>((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 });
});
});