718 lines
30 KiB
TypeScript
718 lines
30 KiB
TypeScript
import { createRequire } from "node:module";
|
|
import { expect, test } from "@playwright/test";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const axePath = require.resolve("axe-core/axe.min.js");
|
|
|
|
async function expectNoAccessibilityViolations(page: import("@playwright/test").Page) {
|
|
await page.addScriptTag({ path: axePath });
|
|
const violations = await page.evaluate(async () => {
|
|
const axe = (window as typeof window & { axe: { run: (options: unknown) => Promise<{ violations: Array<{ id: string; impact?: string | null; help: string; nodes: Array<{ target: unknown; failureSummary?: string }> }> }> } }).axe;
|
|
const result = await axe.run({ runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21aa"] } });
|
|
return result.violations.map(({ id, impact, help, nodes }) => ({
|
|
id,
|
|
impact,
|
|
help,
|
|
nodes: nodes.map((node: { target: unknown; failureSummary?: string }) => ({ target: node.target, failureSummary: node.failureSummary }))
|
|
}));
|
|
});
|
|
expect(violations).toEqual([]);
|
|
}
|
|
|
|
for (const viewport of [
|
|
{ name: "desktop", width: 1280, height: 900 },
|
|
{ name: "mobile", width: 390, height: 844 }
|
|
]) {
|
|
test(`resident permit self-service is keyboard and accessibility conformant on ${viewport.name}`, async ({ page }) => {
|
|
const journey = await mockPublicResidentPermitJourney(page);
|
|
await page.setViewportSize(viewport);
|
|
await page.goto("/forms/public/resident-parking-permit?theme=light");
|
|
|
|
await expect(page.getByRole("heading", { level: 1, name: "Anwohnerparkausweis beantragen" })).toBeVisible();
|
|
await expectNoAccessibilityViolations(page);
|
|
|
|
const name = page.getByLabel("Name der antragstellenden Person");
|
|
await name.focus();
|
|
await page.keyboard.type("Ada Lovelace");
|
|
await page.keyboard.press("Tab");
|
|
await expect(page.getByLabel("E-Mail-Adresse")).toBeFocused();
|
|
await page.keyboard.type("ada.lovelace@example.test");
|
|
await page.keyboard.press("Tab");
|
|
await expect(page.getByLabel("Hauptwohnsitz")).toBeFocused();
|
|
await page.keyboard.type("Musterstraße 17, 10115 Berlin");
|
|
await page.keyboard.press("Tab");
|
|
await expect(page.getByLabel("Kfz-Kennzeichen")).toBeFocused();
|
|
await page.keyboard.type("B-AL 1843");
|
|
await page.keyboard.press("Tab");
|
|
await expect(page.getByRole("button", { name: "Entwurf speichern" })).toBeFocused();
|
|
await page.keyboard.press("Enter");
|
|
await expect.poll(() => journey.savedValues()).toEqual({
|
|
applicant_name: "Ada Lovelace",
|
|
applicant_email: "ada.lovelace@example.test",
|
|
residence_address: "Musterstraße 17, 10115 Berlin",
|
|
licence_plate: "B-AL 1843"
|
|
});
|
|
|
|
await page.getByRole("button", { name: "Absenden" }).click();
|
|
const confirm = page.getByRole("alertdialog", { name: "Formular absenden" });
|
|
await expect(confirm).toBeVisible();
|
|
await expectNoAccessibilityViolations(page);
|
|
await confirm.getByRole("button", { name: "Absenden" }).click();
|
|
await expect(page.getByText("Übermittlung eingegangen")).toBeVisible();
|
|
await expect(page.getByText("receipt-rpp-2026-0001")).toBeVisible();
|
|
await expectNoHorizontalOverflow(page);
|
|
});
|
|
|
|
test(`resident permit assisted intake preserves per-field provenance on ${viewport.name}`, async ({ page }) => {
|
|
const journey = await mockAssistedResidentPermitJourney(page);
|
|
await page.setViewportSize(viewport);
|
|
await page.goto("/forms-runtime?theme=light");
|
|
|
|
await page.getByRole("button", { name: "Assistierte Erfassung" }).click();
|
|
const startDialog = page.getByRole("dialog", { name: "Assistierte Erfassung starten" });
|
|
await expect(startDialog).toBeVisible();
|
|
await expectNoAccessibilityViolations(page);
|
|
await startDialog.getByLabel("Referenz der betroffenen Partei").fill("party:resident-ada-lovelace");
|
|
await startDialog.getByLabel("Referenz der zuständigen Funktion").fill("function:parking-permits");
|
|
await startDialog.getByLabel("Zweck").fill("Anwohnerparkausweis beantragen");
|
|
await startDialog.getByLabel("Referenz der Rechtsgrundlage").fill("law:resident-parking-permit");
|
|
await startDialog.getByLabel("Barrierefreiheits- oder Kommunikationsunterstützung").fill("Leichte Sprache");
|
|
const notice = startDialog.getByRole("checkbox", { name: "Datenschutz- und Verfahrenshinweis wurde erteilt" });
|
|
await notice.focus();
|
|
await page.keyboard.press("Space");
|
|
await expect(notice).toBeChecked();
|
|
await startDialog.getByLabel("Referenz der betroffenen Partei").focus();
|
|
await page.keyboard.press("Tab");
|
|
await expect(page.locator(":focus")).toHaveAttribute("aria-label", "Feldhilfe anzeigen");
|
|
await page.keyboard.press("Tab");
|
|
await expect(startDialog.getByLabel("Referenz der vertretenen Partei")).toBeFocused();
|
|
await startDialog.getByRole("button", { name: "Sitzung starten" }).click();
|
|
|
|
await expect(page).toHaveURL(/\/forms-runtime\/assisted-rpp-1$/);
|
|
await expect(page.getByRole("heading", { level: 1, name: "Anwohnerparkausweis beantragen" })).toBeVisible();
|
|
await page.getByLabel("Name der antragstellenden Person").fill("Ada Lovelace");
|
|
await page.getByLabel("E-Mail-Adresse").fill("ada.lovelace@example.test");
|
|
await page.getByLabel("Hauptwohnsitz").fill("Musterstraße 17, 10115 Berlin");
|
|
await page.getByLabel("Kfz-Kennzeichen").fill("B-AL 1843");
|
|
await page.getByLabel("Änderungsgrund").fill("Angaben gemeinsam mit der antragstellenden Person erfasst.");
|
|
await page.getByRole("button", { name: "Entwurf speichern" }).click();
|
|
|
|
await page.getByRole("button", { name: "Rücklesen und absenden" }).click();
|
|
const readback = page.getByRole("dialog", { name: "Assistiertes Rücklesen erfassen" });
|
|
await expect(readback).toBeVisible();
|
|
await expect(readback.getByRole("group", { name: "Hauptwohnsitz" })).toBeVisible();
|
|
await expectNoAccessibilityViolations(page);
|
|
|
|
const addressSource = readback.getByRole("group", { name: "Hauptwohnsitz" });
|
|
await addressSource.getByLabel("Wertquelle").selectOption("document");
|
|
await addressSource.getByLabel("Quellenvertrauen").selectOption("verified");
|
|
await addressSource.getByLabel("Erklärende Partei oder Quellenreferenz").fill("files:residence-proof-2026");
|
|
const plateSource = readback.getByRole("group", { name: "Kfz-Kennzeichen" });
|
|
await plateSource.getByLabel("Wertquelle").focus();
|
|
await page.keyboard.press("ArrowDown");
|
|
await page.keyboard.press("ArrowDown");
|
|
await page.keyboard.press("ArrowDown");
|
|
await page.keyboard.press("Enter");
|
|
await plateSource.getByLabel("Quellenvertrauen").selectOption("verified");
|
|
await plateSource.getByLabel("Erklärende Partei oder Quellenreferenz").fill("register:vehicle-B-AL-1843");
|
|
await readback.getByRole("button", { name: "Erfassen und fortfahren" }).click();
|
|
|
|
await expect.poll(() => journey.confirmationSources()).toMatchObject({
|
|
applicant_name: { source: "person_statement", confidence: "stated" },
|
|
residence_address: {
|
|
source: "document",
|
|
confidence: "verified",
|
|
declared_by_ref: "files:residence-proof-2026"
|
|
},
|
|
licence_plate: {
|
|
source: "system",
|
|
confidence: "verified",
|
|
declared_by_ref: "register:vehicle-B-AL-1843"
|
|
}
|
|
});
|
|
const submit = page.getByRole("alertdialog", { name: "Formular absenden" });
|
|
await submit.getByRole("button", { name: "Absenden" }).click();
|
|
await expect(page.getByText("receipt-assisted-rpp-2026-0001")).toBeVisible();
|
|
await expect(page.getByText("Rücklesen erfasst")).toBeVisible();
|
|
await expectNoHorizontalOverflow(page);
|
|
});
|
|
}
|
|
|
|
test("shared components remain accessible and keyboard operable", async ({ page }) => {
|
|
await page.goto("/?theme=light");
|
|
await expect(page.getByRole("heading", { level: 1, name: "Zentrale GovOPlaN-Oberflächen" })).toBeVisible();
|
|
await expectNoAccessibilityViolations(page);
|
|
|
|
const editorActions = page.getByRole("toolbar", { name: "Bearbeitungsaktionen" });
|
|
await expect(page.locator("[data-page-archetype='overview']")).toBeVisible();
|
|
await expect(editorActions).toHaveAttribute("data-page-refreshable", "true");
|
|
await expect(editorActions).toHaveAttribute("data-page-dirty", "true");
|
|
await expect(editorActions.getByRole("status")).toHaveText("Unsaved changes");
|
|
await expect(editorActions.locator("[data-page-action-separation='destructive']")).toHaveCount(1);
|
|
await expect(editorActions.locator("[data-page-action-separation='destructive']")).toHaveCSS("border-left-width", "2px");
|
|
const actionSlots = await editorActions.locator("[data-page-action-slot]").evaluateAll((elements) => elements.map((element) => element.getAttribute("data-page-action-slot")));
|
|
expect(actionSlots).toEqual([
|
|
"reload",
|
|
"context",
|
|
"destructive",
|
|
"discard",
|
|
"save"
|
|
]);
|
|
await expect(editorActions.getByRole("button")).toHaveText([
|
|
"Neu laden",
|
|
"Vorschau öffnen",
|
|
"Löschen",
|
|
"Verwerfen",
|
|
"Änderungen speichern"
|
|
]);
|
|
await editorActions.getByRole("button", { name: "Neu laden" }).focus();
|
|
await page.keyboard.press("Tab");
|
|
await expect(editorActions.getByRole("button", { name: "Vorschau öffnen" })).toBeFocused();
|
|
await page.keyboard.press("Tab");
|
|
await expect(editorActions.locator(".disabled-action-tooltip")).toBeFocused();
|
|
await expect(page.getByRole("tooltip")).toContainText("Nur die federführende Stelle");
|
|
await page.keyboard.press("Tab");
|
|
await expect(editorActions.getByRole("button", { name: "Verwerfen" })).toBeFocused();
|
|
await page.keyboard.press("Tab");
|
|
await expect(editorActions.getByRole("button", { name: "Änderungen speichern" })).toBeFocused();
|
|
await page.keyboard.press("Enter");
|
|
await expect(editorActions).toHaveAttribute("data-page-dirty", "false");
|
|
await expect(editorActions.getByRole("status")).toHaveText("Saved");
|
|
await expect(editorActions.getByRole("button", { name: "Verwerfen" })).toBeDisabled();
|
|
await expect(editorActions.getByRole("button", { name: "Änderungen speichern" })).toBeDisabled();
|
|
|
|
const metricDrilldown = page.getByRole("button", { name: "Offene Aufgaben prüfen" });
|
|
await metricDrilldown.focus();
|
|
await page.keyboard.press("Enter");
|
|
await expect(page.getByTestId("metric-drilldown-result")).toHaveText("18 offene Aufgaben");
|
|
|
|
const opener = page.getByTestId("open-dialog");
|
|
await opener.focus();
|
|
await page.keyboard.press("Enter");
|
|
await expect(page.getByRole("dialog", { name: "Konsequenz vor Ausführung prüfen" })).toBeVisible();
|
|
await expectNoAccessibilityViolations(page);
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByRole("dialog")).toBeHidden();
|
|
await expect(opener).toBeFocused();
|
|
});
|
|
|
|
test("full-page tool launch preserves a bounded return context", async ({ page }) => {
|
|
await page.goto("/?theme=light");
|
|
await page.getByTestId("launch-full-page").click();
|
|
await expect(page).toHaveURL(/\/files$/);
|
|
const returnButton = page.getByRole("button", { name: /RPP-2026-0001.*Anwohnerparkausweis/ });
|
|
await expect(returnButton).toBeVisible();
|
|
await returnButton.click();
|
|
await expect(page).toHaveURL(/\/?\?theme=light$/);
|
|
});
|
|
|
|
test("Quick Access preserves focus and adapts to a narrow viewport", async ({ page }) => {
|
|
await page.route("**/api/v1/quick-access/effective*", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify(quickAccessPayload(false))
|
|
});
|
|
});
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await page.goto("/?theme=light&quick-access=1");
|
|
const filesTrigger = page.getByRole("button", { name: "Files" });
|
|
await expect(filesTrigger).toBeVisible();
|
|
await filesTrigger.click();
|
|
const drawer = page.getByRole("dialog", { name: "Files" });
|
|
await expect(drawer).toBeVisible();
|
|
await expect(drawer.getByRole("button", { name: "Close" })).toBeFocused();
|
|
const drawerBounds = await drawer.boundingBox();
|
|
expect(drawerBounds?.x).toBeGreaterThanOrEqual(0);
|
|
expect((drawerBounds?.x ?? 0) + (drawerBounds?.width ?? 0)).toBeLessThanOrEqual(390);
|
|
await page.keyboard.press("Escape");
|
|
await expect(drawer).toBeHidden();
|
|
await expect(filesTrigger).toBeFocused();
|
|
});
|
|
|
|
test("F1 resolves focused high-risk help and restores focus accessibly", async ({ page }) => {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await page.goto("/?theme=light&help=1");
|
|
|
|
const retentionAction = page.getByTestId("f1-retention-action");
|
|
await retentionAction.focus();
|
|
await page.keyboard.press("F1");
|
|
|
|
const dialog = page.getByRole("dialog", { name: "Kontexthilfe" });
|
|
await expect(dialog).toBeVisible();
|
|
await expect(dialog.locator("[data-help-context]"))
|
|
.toHaveAttribute("data-help-context", "policy.retention.action.apply");
|
|
await expect(dialog.getByRole("heading", { level: 3 }))
|
|
.toHaveText("Aufbewahrungsregeln anwenden");
|
|
await expect(dialog).toContainText("topic=policy.admin.retention-and-disposition");
|
|
await expect(dialog.getByRole("button", { name: "Administrator-Dokumentation öffnen" }))
|
|
.toBeVisible();
|
|
await expectNoAccessibilityViolations(page);
|
|
|
|
const bounds = await dialog.boundingBox();
|
|
expect(bounds?.x).toBeGreaterThanOrEqual(0);
|
|
expect((bounds?.x ?? 0) + (bounds?.width ?? 0)).toBeLessThanOrEqual(390);
|
|
|
|
await page.keyboard.press("Escape");
|
|
await expect(dialog).toBeHidden();
|
|
await expect(retentionAction).toBeFocused();
|
|
|
|
const fallbackAction = page.getByTestId("f1-fallback-action");
|
|
await fallbackAction.focus();
|
|
await page.keyboard.press("F1");
|
|
const fallbackDialog = page.getByRole("dialog", { name: "Kontexthilfe" });
|
|
await expect(fallbackDialog.locator("[data-help-context]"))
|
|
.toHaveAttribute("data-help-context", "core.conformance.action.review");
|
|
await expect(fallbackDialog).toContainText("fallback_context=campaigns.list");
|
|
await page.keyboard.press("Escape");
|
|
await expect(fallbackAction).toBeFocused();
|
|
});
|
|
|
|
test("View focus has a deliberate permission-derived all-tools escape", async ({ page }) => {
|
|
await page.route("**/api/v1/quick-access/effective*", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify(quickAccessPayload(true))
|
|
});
|
|
});
|
|
await page.goto("/?theme=light&quick-access=focused");
|
|
|
|
const rail = page.locator(".quick-access-rail");
|
|
await expect(rail).toHaveAttribute("data-view-focus-mode", "focused");
|
|
await expect(page.getByRole("button", { name: "Files" })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Messages" })).toHaveCount(0);
|
|
|
|
const allTools = page.locator(".quick-access-view-mode-button");
|
|
await expect(allTools).toHaveAttribute("aria-pressed", "false");
|
|
await allTools.click();
|
|
await expect(rail).toHaveAttribute("data-view-focus-mode", "all");
|
|
await expect(page.getByRole("button", { name: "Messages" })).toBeVisible();
|
|
|
|
await page.getByRole("button", { name: "Messages" }).click();
|
|
const explanation = page.locator(".quick-access-view-mode[role='status']");
|
|
await expect(explanation).toHaveAttribute("data-view-focus-mode", "all");
|
|
await explanation.getByRole("button").click();
|
|
await expect(rail).toHaveAttribute("data-view-focus-mode", "focused");
|
|
await expect(page.getByRole("button", { name: "Messages" })).toHaveCount(0);
|
|
});
|
|
|
|
test("a stale View focus falls back safely in a sparse optional-module catalogue", async ({ page }) => {
|
|
await page.route("**/api/v1/quick-access/effective*", async (route) => {
|
|
await route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify(quickAccessPayload(false))
|
|
});
|
|
});
|
|
await page.goto("/?theme=light&quick-access=stale-focus");
|
|
|
|
const rail = page.locator(".quick-access-rail");
|
|
await expect(rail).toHaveAttribute("data-view-focus-mode", "unavailable");
|
|
await expect(page.getByRole("button", { name: "Files" })).toBeVisible();
|
|
await expect(page.locator(".quick-access-view-mode-button")).toHaveCount(0);
|
|
await page.getByRole("button", { name: "Files" }).click();
|
|
await expect(page.locator(".quick-access-view-mode[role='status']"))
|
|
.toHaveAttribute("data-view-focus-mode", "unavailable");
|
|
});
|
|
|
|
test("light and dark desktop geometry remains stable", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1440, height: 1000 });
|
|
await page.goto("/?theme=light");
|
|
await expect(page.locator("[data-conformance-id='shared-ui-lab']")).toHaveScreenshot("shared-ui-light-desktop.png", { animations: "disabled", maxDiffPixelRatio: 0.005 });
|
|
|
|
await page.goto("/?theme=dark");
|
|
await expect(page.locator("[data-conformance-id='shared-ui-lab']")).toHaveScreenshot("shared-ui-dark-desktop.png", { animations: "disabled", maxDiffPixelRatio: 0.005 });
|
|
});
|
|
|
|
test("narrow layout preserves task order without horizontal overflow", async ({ page }) => {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await page.goto("/?theme=light");
|
|
await expect(page.getByText("Bedingung prüfen")).toBeVisible();
|
|
const overflowing = await page.evaluate(() => Array.from(document.querySelectorAll<HTMLElement>("body *"))
|
|
.map((element) => {
|
|
const rect = element.getBoundingClientRect();
|
|
return {
|
|
element: `${element.tagName.toLowerCase()}.${Array.from(element.classList).join(".")}`,
|
|
left: Math.round(rect.left),
|
|
right: Math.round(rect.right),
|
|
scrollWidth: element.scrollWidth,
|
|
clientWidth: element.clientWidth
|
|
};
|
|
})
|
|
.filter(({ left, right }) => left < -1 || right > window.innerWidth + 1)
|
|
.slice(0, 20));
|
|
expect(overflowing).toEqual([]);
|
|
await expect(page.locator("[data-page-action-archetype='editor'] [data-page-action-group='trailing']"))
|
|
.toHaveCSS("justify-content", "flex-end");
|
|
const saveRightEdge = await page.locator("[data-page-action-archetype='editor'] [data-page-action-slot='save']").evaluate((element) => element.getBoundingClientRect().right);
|
|
const actionBarRightEdge = await page.locator("[data-page-action-archetype='editor']").evaluate((element) => element.getBoundingClientRect().right);
|
|
expect(Math.abs(actionBarRightEdge - saveRightEdge)).toBeLessThanOrEqual(1);
|
|
await expect(page.locator("[data-conformance-id='shared-ui-lab']")).toHaveScreenshot("shared-ui-light-narrow.png", { animations: "disabled", maxDiffPixelRatio: 0.005 });
|
|
});
|
|
|
|
async function expectNoHorizontalOverflow(page: import("@playwright/test").Page) {
|
|
const overflowing = await page.evaluate(() => Array.from(document.querySelectorAll<HTMLElement>("body *"))
|
|
.filter((element) => {
|
|
const style = window.getComputedStyle(element);
|
|
return style.display !== "none" && style.visibility !== "hidden";
|
|
})
|
|
.map((element) => {
|
|
const rect = element.getBoundingClientRect();
|
|
return {
|
|
element: `${element.tagName.toLowerCase()}.${Array.from(element.classList).join(".")}`,
|
|
left: Math.round(rect.left),
|
|
right: Math.round(rect.right)
|
|
};
|
|
})
|
|
.filter(({ left, right }) => left < -1 || right > window.innerWidth + 1)
|
|
.slice(0, 20));
|
|
expect(overflowing).toEqual([]);
|
|
}
|
|
|
|
async function mockPublicResidentPermitJourney(page: import("@playwright/test").Page) {
|
|
let current = residentPermitInstance("public-rpp-1", "started", 1, {});
|
|
let savedValues: Record<string, unknown> = {};
|
|
|
|
await page.route("**/api/v1/forms-runtime/**", async (route) => {
|
|
const request = route.request();
|
|
const path = new URL(request.url()).pathname;
|
|
const method = request.method();
|
|
if (path.endsWith("/public/profiles/resident-parking-permit/start") && method === "POST") {
|
|
return fulfillJson(route, {
|
|
session_id: "session-public-rpp-1",
|
|
mode: "anonymous",
|
|
status: "active",
|
|
expires_at: "2026-08-25T10:00:00Z",
|
|
instance: current,
|
|
token: "public-rpp-token",
|
|
replayed: false
|
|
});
|
|
}
|
|
if (path.endsWith("/public/intake") && method === "GET") {
|
|
return fulfillJson(route, { instance: current, definition: residentPermitDefinition() });
|
|
}
|
|
if (path.endsWith("/public/intake") && method === "PATCH") {
|
|
const payload = request.postDataJSON() as { values: Record<string, unknown> };
|
|
savedValues = payload.values;
|
|
current = residentPermitInstance("public-rpp-1", "draft", 2, payload.values);
|
|
return fulfillJson(route, current);
|
|
}
|
|
if (path.endsWith("/public/intake/submit") && method === "POST") {
|
|
const payload = request.postDataJSON() as { values: Record<string, unknown> };
|
|
current = {
|
|
...residentPermitInstance("public-rpp-1", "submitted", 3, payload.values),
|
|
receipt_id: "receipt-rpp-2026-0001"
|
|
};
|
|
return fulfillJson(route, current);
|
|
}
|
|
return route.abort("failed");
|
|
});
|
|
|
|
return { savedValues: () => savedValues };
|
|
}
|
|
|
|
async function mockAssistedResidentPermitJourney(page: import("@playwright/test").Page) {
|
|
let current = residentPermitInstance("assisted-rpp-1", "started", 1, {}, true);
|
|
let confirmationSources: Record<string, unknown> = {};
|
|
let confirmations: unknown[] = [];
|
|
|
|
await page.route("**/api/v1/forms-runtime/**", async (route) => {
|
|
const request = route.request();
|
|
const path = new URL(request.url()).pathname;
|
|
const method = request.method();
|
|
|
|
if (path.endsWith("/instances") && method === "GET") {
|
|
return fulfillJson(route, { instances: [], total: 0, offset: 0, limit: 200 });
|
|
}
|
|
if (path.endsWith("/assisted-intake/profiles") && method === "GET") {
|
|
return fulfillJson(route, { profiles: [{
|
|
profile_id: "assisted-profile-rpp",
|
|
public_id: "resident-parking-permit",
|
|
definition_ref: residentPermitDefinition().reference,
|
|
mode: "assisted",
|
|
enabled: true,
|
|
revision: 1,
|
|
draft_ttl_seconds: 2_592_000,
|
|
invitation_ttl_seconds: 1_209_600,
|
|
rate_limit_per_minute: 60,
|
|
metadata: { definition_title: "Anwohnerparkausweis beantragen" }
|
|
}] });
|
|
}
|
|
if (path.endsWith("/assisted-intake/start") && method === "POST") {
|
|
return fulfillJson(route, {
|
|
session_id: "assisted-session-rpp-1",
|
|
mode: "assisted",
|
|
status: "active",
|
|
expires_at: "2026-08-25T10:00:00Z",
|
|
instance: current,
|
|
token: null,
|
|
replayed: false
|
|
});
|
|
}
|
|
if (path.endsWith("/instances/assisted-rpp-1/definition") && method === "GET") {
|
|
return fulfillJson(route, residentPermitDefinition());
|
|
}
|
|
if (path.endsWith("/instances/assisted-rpp-1/history") && method === "GET") {
|
|
return fulfillJson(route, { revisions: [current] });
|
|
}
|
|
if (path.endsWith("/instances/assisted-rpp-1/events") && method === "GET") {
|
|
return fulfillJson(route, { events: [{
|
|
event_id: `event-${current.revision}`,
|
|
event_type: current.status === "submitted" ? "submitted" : "draft_saved",
|
|
instance_revision: current.revision,
|
|
status: current.status,
|
|
occurred_at: current.recorded_at,
|
|
actor_id: "operator-1",
|
|
payload: {}
|
|
}] });
|
|
}
|
|
if (path.endsWith("/instances/assisted-rpp-1/handoffs") && method === "GET") {
|
|
return fulfillJson(route, { handoffs: [] });
|
|
}
|
|
if (path.endsWith("/instances/assisted-rpp-1/assisted-confirmations") && method === "GET") {
|
|
return fulfillJson(route, { confirmations });
|
|
}
|
|
if (path.endsWith("/instances/assisted-rpp-1/assisted-confirmations") && method === "POST") {
|
|
const payload = request.postDataJSON() as { field_sources: Record<string, unknown> };
|
|
confirmationSources = payload.field_sources;
|
|
const confirmation = {
|
|
confirmation_id: "confirmation-rpp-1",
|
|
instance_id: "assisted-rpp-1",
|
|
instance_revision: current.revision,
|
|
outcome: "confirmed",
|
|
method: "spoken_readback",
|
|
confirmed_by_ref: "party:resident-ada-lovelace",
|
|
operator_actor_id: "operator-1",
|
|
confirmed_at: "2026-08-24T10:10:00Z",
|
|
payload_sha256: "a".repeat(64),
|
|
correction_note: null,
|
|
metadata: { field_sources: confirmationSources }
|
|
};
|
|
confirmations = [confirmation];
|
|
return fulfillJson(route, confirmation);
|
|
}
|
|
if (path.endsWith("/instances/assisted-rpp-1/submit") && method === "POST") {
|
|
const payload = request.postDataJSON() as { values: Record<string, unknown> };
|
|
current = {
|
|
...residentPermitInstance("assisted-rpp-1", "submitted", current.revision + 1, payload.values, true),
|
|
receipt_id: "receipt-assisted-rpp-2026-0001"
|
|
};
|
|
return fulfillJson(route, current);
|
|
}
|
|
if (path.endsWith("/instances/assisted-rpp-1") && method === "PATCH") {
|
|
const payload = request.postDataJSON() as { values: Record<string, unknown> };
|
|
current = residentPermitInstance("assisted-rpp-1", "draft", current.revision + 1, payload.values, true);
|
|
return fulfillJson(route, current);
|
|
}
|
|
if (path.endsWith("/instances/assisted-rpp-1") && method === "GET") {
|
|
return fulfillJson(route, current);
|
|
}
|
|
return route.abort("failed");
|
|
});
|
|
|
|
return { confirmationSources: () => confirmationSources };
|
|
}
|
|
|
|
function residentPermitDefinition() {
|
|
return {
|
|
reference: {
|
|
kind: "form",
|
|
owner_module: "forms",
|
|
object_id: "resident-parking-permit-application",
|
|
tenant_id: "tenant-1",
|
|
version: "3",
|
|
label: "Anwohnerparkausweis beantragen"
|
|
},
|
|
key: "resident_parking_permit.apply",
|
|
temporal: { revision: "3", recorded_at: "2026-08-24T10:00:00Z" },
|
|
title: "Resident parking permit application",
|
|
description: "Apply digitally or together with an authorized service worker.",
|
|
fields: [
|
|
residentPermitField("applicant_name", "Applicant name", "text", { min_length: 2, max_length: 200 }),
|
|
residentPermitField("applicant_email", "Applicant email", "email", { format: "email" }),
|
|
residentPermitField("residence_address", "Primary residence", "text", { max_length: 500 }),
|
|
residentPermitField("licence_plate", "Licence plate", "text", { max_length: 20 })
|
|
],
|
|
publication_state: "published",
|
|
allow_drafts: true,
|
|
max_attachments: 0,
|
|
signature_requirement: "none",
|
|
policy_refs: ["law:resident-parking-permit"],
|
|
handoff_kinds: [],
|
|
fallback_locale: "de",
|
|
localizations: [{
|
|
locale: "de",
|
|
title: "Anwohnerparkausweis beantragen",
|
|
description: "Beantragen Sie den Anwohnerparkausweis digital oder gemeinsam mit einer berechtigten Servicestelle.",
|
|
field_labels: {
|
|
applicant_name: "Name der antragstellenden Person",
|
|
applicant_email: "E-Mail-Adresse",
|
|
residence_address: "Hauptwohnsitz",
|
|
licence_plate: "Kfz-Kennzeichen"
|
|
},
|
|
field_help_texts: {},
|
|
option_labels: {},
|
|
page_titles: {},
|
|
section_titles: {}
|
|
}]
|
|
};
|
|
}
|
|
|
|
function residentPermitField(
|
|
key: string,
|
|
label: string,
|
|
valueType: "text" | "email",
|
|
constraints: Record<string, unknown>
|
|
) {
|
|
return {
|
|
key,
|
|
label,
|
|
value_type: valueType,
|
|
required: true,
|
|
help_text: null,
|
|
options: [],
|
|
constraints,
|
|
default_value: null,
|
|
visibility_condition: null
|
|
};
|
|
}
|
|
|
|
function residentPermitInstance(
|
|
instanceId: string,
|
|
status: string,
|
|
revision: number,
|
|
values: Record<string, unknown>,
|
|
assisted = false
|
|
) {
|
|
return {
|
|
reference: {
|
|
kind: "form_instance",
|
|
owner_module: "forms_runtime",
|
|
object_id: instanceId,
|
|
tenant_id: "tenant-1",
|
|
version: String(revision),
|
|
label: "Anwohnerparkausweis beantragen"
|
|
},
|
|
tenant_id: "tenant-1",
|
|
instance_id: instanceId,
|
|
revision,
|
|
status,
|
|
definition_ref: residentPermitDefinition().reference,
|
|
values,
|
|
validation_results: [],
|
|
attachment_refs: [],
|
|
signature_refs: [],
|
|
handoff_refs: [],
|
|
service_ref: null,
|
|
receipt_id: null as string | null,
|
|
recorded_at: "2026-08-24T10:00:00Z",
|
|
change_reason: revision === 1 ? "Assisted session started." : "Draft saved.",
|
|
created_by: "operator-1",
|
|
changed_by: "operator-1",
|
|
metadata: assisted ? {
|
|
intake: {
|
|
session_id: "assisted-session-rpp-1",
|
|
profile_id: "assisted-profile-rpp",
|
|
mode: "assisted",
|
|
channel: "counter",
|
|
affected_party_ref: "party:resident-ada-lovelace",
|
|
represented_party_ref: null,
|
|
authority_basis: "self",
|
|
purpose: "Anwohnerparkausweis beantragen",
|
|
legal_basis_ref: "law:resident-parking-permit",
|
|
consent_basis: "in-person-confirmation",
|
|
notice_given: true,
|
|
responsible_function_ref: "function:parking-permits",
|
|
language: "de",
|
|
accessibility_needs: ["Leichte Sprache"],
|
|
field_sources: {},
|
|
operator: { actor_id: "operator-1", auth_method: "session" }
|
|
}
|
|
} : {},
|
|
status_access: null,
|
|
replayed: false
|
|
};
|
|
}
|
|
|
|
async function fulfillJson(
|
|
route: import("@playwright/test").Route,
|
|
body: unknown
|
|
) {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body)
|
|
});
|
|
}
|
|
|
|
function quickAccessPayload(includeMessages: boolean) {
|
|
const files = {
|
|
id: "files",
|
|
label: "Files",
|
|
description: "Files beside the current task",
|
|
icon: "files",
|
|
order: 10,
|
|
enabled: true,
|
|
forced: false,
|
|
locked_by: null,
|
|
tools: [{
|
|
contract_version: "1",
|
|
id: "files.recent",
|
|
module_id: "files",
|
|
category_id: "files",
|
|
label: "Recent files",
|
|
description: "Select an authorized file without leaving this case.",
|
|
icon: "files",
|
|
surface_id: "files.route.files",
|
|
full_page_path: "/files",
|
|
required_all: [],
|
|
required_any: [],
|
|
order: 10,
|
|
default_enabled: true,
|
|
modes: ["select"],
|
|
availability: "global",
|
|
accepted_reference_kinds: [],
|
|
returned_reference_kinds: ["files.file-version"],
|
|
help_context_id: "files.quick_access.files",
|
|
enabled: true,
|
|
forced: false,
|
|
locked_by: null
|
|
}]
|
|
};
|
|
const messages = {
|
|
id: "messages",
|
|
label: "Messages",
|
|
description: "Messages beside the current task",
|
|
icon: "messages",
|
|
order: 20,
|
|
enabled: true,
|
|
forced: false,
|
|
locked_by: null,
|
|
tools: [{
|
|
contract_version: "1",
|
|
id: "postbox.unread",
|
|
module_id: "postbox",
|
|
category_id: "messages",
|
|
label: "Unread messages",
|
|
description: "Open an authorized unread message.",
|
|
icon: "messages",
|
|
surface_id: "postbox.route.postbox",
|
|
full_page_path: "/postbox",
|
|
required_all: [],
|
|
required_any: [],
|
|
order: 10,
|
|
default_enabled: true,
|
|
modes: ["view"],
|
|
availability: "global",
|
|
accepted_reference_kinds: [],
|
|
returned_reference_kinds: ["postbox.message"],
|
|
help_context_id: "postbox.quick_access.messages",
|
|
enabled: true,
|
|
forced: false,
|
|
locked_by: null
|
|
}]
|
|
};
|
|
return {
|
|
categories: includeMessages ? [files, messages] : [files],
|
|
diagnostics: []
|
|
};
|
|
}
|