Exercise accessible resident permit journeys
Module Package Release / publish-packages (push) Successful in 13s

This commit is contained in:
2026-08-24 14:03:43 +02:00
parent 6e518fa6a2
commit 08c3e47b6d
15 changed files with 511 additions and 23 deletions
@@ -19,6 +19,125 @@ async function expectNoAccessibilityViolations(page: import("@playwright/test").
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();
@@ -229,6 +348,301 @@ test("narrow layout preserves task order without horizontal overflow", async ({
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",