152 lines
7.9 KiB
TypeScript
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 });
|
|
});
|
|
});
|