Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08c3e47b6d |
@@ -103,6 +103,14 @@ modal at narrow widths, closes with Escape, and restores focus to the triggering
|
|||||||
control. Module journeys should add their own exact high-risk mappings; they do
|
control. Module journeys should add their own exact high-risk mappings; they do
|
||||||
not need to reimplement the keyboard or dialog mechanics.
|
not need to reimplement the keyboard or dialog mechanics.
|
||||||
|
|
||||||
|
The same conformance suite mounts the production Forms Runtime self-service and
|
||||||
|
assisted Anwohnerparkausweis surfaces with German module translations. Desktop
|
||||||
|
and mobile runs traverse native controls by keyboard, inspect accessible names
|
||||||
|
and landmarks, run WCAG 2.1 A/AA automation, verify responsive overflow, and
|
||||||
|
retain independent per-field assisted provenance. Physical assistive-technology
|
||||||
|
spot checks remain release evidence rather than being represented as browser
|
||||||
|
automation.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -236,7 +236,9 @@ instead of reproducing their behavior.
|
|||||||
not self-explanatory.
|
not self-explanatory.
|
||||||
- `help` content is contextual guidance, not the accessible name. The persisted
|
- `help` content is contextual guidance, not the accessible name. The persisted
|
||||||
`show_inline_help_hints` user preference hides only the `InlineHelp` marker by
|
`show_inline_help_hints` user preference hides only the `InlineHelp` marker by
|
||||||
applying `ui-hide-help-hints` at the document root.
|
applying `ui-hide-help-hints` at the document root. When shown, the shared
|
||||||
|
marker is a labelled, keyboard-focusable help control and exposes its tooltip
|
||||||
|
on focus as well as pointer hover.
|
||||||
- Shared action-bearing components accept an optional disabled reason. In
|
- Shared action-bearing components accept an optional disabled reason. In
|
||||||
particular, `MailServerSettingsPanel` forwards protocol-specific test
|
particular, `MailServerSettingsPanel` forwards protocol-specific test
|
||||||
blockers into the shared focusable disabled-action tooltip; modules provide
|
blockers into the shared focusable disabled-action tooltip; modules provide
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-core"
|
name = "govoplan-core"
|
||||||
version = "0.1.40"
|
version = "0.1.41"
|
||||||
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
|
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { FileText, GitBranch, Inbox, Search, ShieldCheck } from "lucide-react";
|
import { FileText, GitBranch, Inbox, Search, ShieldCheck } from "lucide-react";
|
||||||
import { useLocation } from "react-router";
|
import { useLocation } from "react-router";
|
||||||
|
import FormInstancePage from "../../../govoplan-forms-runtime/webui/src/features/forms/FormInstancePage";
|
||||||
|
import FormsRuntimePage from "../../../govoplan-forms-runtime/webui/src/features/forms/FormsRuntimePage";
|
||||||
|
import PublicFormPage from "../../../govoplan-forms-runtime/webui/src/features/forms/PublicFormPage";
|
||||||
import QuickAccessRail from "../../../govoplan-quick-access/webui/src/components/QuickAccessRail";
|
import QuickAccessRail from "../../../govoplan-quick-access/webui/src/components/QuickAccessRail";
|
||||||
import ActionToolbar from "../src/components/ActionToolbar";
|
import ActionToolbar from "../src/components/ActionToolbar";
|
||||||
import Button from "../src/components/Button";
|
import Button from "../src/components/Button";
|
||||||
@@ -40,6 +43,16 @@ export default function ConformanceApp() {
|
|||||||
const [editorDirty, setEditorDirty] = useState(true);
|
const [editorDirty, setEditorDirty] = useState(true);
|
||||||
const [metricDrilldown, setMetricDrilldown] = useState("");
|
const [metricDrilldown, setMetricDrilldown] = useState("");
|
||||||
|
|
||||||
|
if (location.pathname.startsWith("/forms/public/")) {
|
||||||
|
return <PublicFormPage settings={CONFORMANCE_SETTINGS} auth={FORMS_RUNTIME_AUTH} />;
|
||||||
|
}
|
||||||
|
if (location.pathname === "/forms-runtime") {
|
||||||
|
return <FormsRuntimePage settings={CONFORMANCE_SETTINGS} auth={FORMS_RUNTIME_AUTH} />;
|
||||||
|
}
|
||||||
|
if (location.pathname.startsWith("/forms-runtime/")) {
|
||||||
|
return <FormInstancePage settings={CONFORMANCE_SETTINGS} auth={FORMS_RUNTIME_AUTH} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="conformance-root" data-conformance-id="shared-ui-lab">
|
<main className="conformance-root" data-conformance-id="shared-ui-lab">
|
||||||
<PageLayout
|
<PageLayout
|
||||||
@@ -290,6 +303,16 @@ const CONFORMANCE_AUTH = {
|
|||||||
groups_loaded: true
|
groups_loaded: true
|
||||||
} satisfies AuthInfo;
|
} satisfies AuthInfo;
|
||||||
|
|
||||||
|
const FORMS_RUNTIME_AUTH = {
|
||||||
|
...CONFORMANCE_AUTH,
|
||||||
|
scopes: [
|
||||||
|
"forms_runtime:submission:assist",
|
||||||
|
"forms_runtime:submission:participate",
|
||||||
|
"forms_runtime:workspace:read",
|
||||||
|
"forms_runtime:workspace:write"
|
||||||
|
]
|
||||||
|
} satisfies AuthInfo;
|
||||||
|
|
||||||
const CONFORMANCE_SETTINGS: ApiSettings = {
|
const CONFORMANCE_SETTINGS: ApiSettings = {
|
||||||
apiBaseUrl: "",
|
apiBaseUrl: "",
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
|
|||||||
@@ -1,20 +1,52 @@
|
|||||||
// Narrow facade used only by the conformance build. It lets the optional
|
// Narrow facade used only by the conformance build. It lets optional modules
|
||||||
// Quick Access module exercise its real rail without pulling the composed
|
// exercise their real task surfaces without pulling the composed application's
|
||||||
// application's generated module catalogue into this isolated test bundle.
|
// generated module catalogue into this isolated test bundle.
|
||||||
export { apiFetch } from "../src/api/client";
|
export { apiFetch, apiPath } from "../src/api/client";
|
||||||
|
export { default as ActionBlockerHint } from "../src/components/ActionBlockerHint";
|
||||||
|
export { default as ActionToolbar } from "../src/components/ActionToolbar";
|
||||||
|
export { default as Button } from "../src/components/Button";
|
||||||
|
export { default as ConfirmDialog } from "../src/components/ConfirmDialog";
|
||||||
|
export { default as DescriptionList, DescriptionItem } from "../src/components/DescriptionList";
|
||||||
|
export { default as Dialog } from "../src/components/Dialog";
|
||||||
|
export { DialogForm, DialogSection } from "../src/components/DialogAnatomy";
|
||||||
export { default as DismissibleAlert } from "../src/components/DismissibleAlert";
|
export { default as DismissibleAlert } from "../src/components/DismissibleAlert";
|
||||||
export { default as DocumentationHelpLink } from "../src/components/help/DocumentationHelpLink";
|
export { default as DocumentationHelpLink } from "../src/components/help/DocumentationHelpLink";
|
||||||
|
export type { DocumentationHelpReference } from "../src/components/help/documentationHelp";
|
||||||
|
export { default as FileDropZone } from "../src/components/FileDropZone";
|
||||||
|
export { default as FormField } from "../src/components/FormField";
|
||||||
|
export { FormGrid } from "../src/components/ContentGrid";
|
||||||
export { default as IconButton } from "../src/components/IconButton";
|
export { default as IconButton } from "../src/components/IconButton";
|
||||||
export { default as LoadingFrame } from "../src/components/LoadingFrame";
|
export { default as LoadingFrame } from "../src/components/LoadingFrame";
|
||||||
export { useGuardedNavigate } from "../src/components/UnsavedChangesGuard";
|
export { default as LoadingIndicator } from "../src/components/LoadingIndicator";
|
||||||
export { usePlatformLanguage } from "../src/i18n/LanguageContext";
|
export { default as PageScrollViewport } from "../src/components/PageScrollViewport";
|
||||||
|
export {
|
||||||
|
default as SelectionList,
|
||||||
|
SelectionListItem,
|
||||||
|
SelectionListItemContent
|
||||||
|
} from "../src/components/SelectionList";
|
||||||
|
export { default as StatePanel } from "../src/components/StatePanel";
|
||||||
|
export { default as StatusBadge } from "../src/components/StatusBadge";
|
||||||
|
export { default as ToggleSwitch } from "../src/components/ToggleSwitch";
|
||||||
|
export {
|
||||||
|
useGuardedNavigate,
|
||||||
|
useUnsavedDraftGuard
|
||||||
|
} from "../src/components/UnsavedChangesGuard";
|
||||||
|
export {
|
||||||
|
i18nMessage,
|
||||||
|
usePlatformLanguage
|
||||||
|
} from "../src/i18n/LanguageContext";
|
||||||
|
export { usePlatformModuleInstalled } from "../src/platform/ModuleContext";
|
||||||
export {
|
export {
|
||||||
dispatchQuickAccessResult,
|
dispatchQuickAccessResult,
|
||||||
quickAccessLaunchState
|
quickAccessLaunchState
|
||||||
} from "../src/platform/launchContext";
|
} from "../src/platform/launchContext";
|
||||||
export { i18nMessage } from "../src/i18n/LanguageContext";
|
export { hasScope } from "../src/utils/permissions";
|
||||||
|
export { default as WorkspaceActionBar } from "../src/components/WorkspaceActionBar";
|
||||||
|
export { default as WorkspaceFrame } from "../src/components/WorkspaceFrame";
|
||||||
export type {
|
export type {
|
||||||
ApiSettings,
|
ApiSettings,
|
||||||
|
PlatformRouteContext,
|
||||||
|
PlatformTranslations,
|
||||||
QuickAccessRailProps,
|
QuickAccessRailProps,
|
||||||
QuickAccessToolsUiCapability
|
QuickAccessToolsUiCapability
|
||||||
} from "../src/types";
|
} from "../src/types";
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { BrowserRouter } from "react-router";
|
import { BrowserRouter, Route, Routes } from "react-router";
|
||||||
import ConformanceApp from "./ConformanceApp";
|
import ConformanceApp from "./ConformanceApp";
|
||||||
|
import { generatedTranslations as formsRuntimeTranslations } from "../../../govoplan-forms-runtime/webui/src/i18n/generatedTranslations";
|
||||||
import { UnsavedChangesProvider } from "../src/components/UnsavedChangesGuard";
|
import { UnsavedChangesProvider } from "../src/components/UnsavedChangesGuard";
|
||||||
import { PlatformLanguageProvider } from "../src/i18n/LanguageContext";
|
import { PlatformLanguageProvider } from "../src/i18n/LanguageContext";
|
||||||
import { PlatformModulesProvider } from "../src/platform/ModuleContext";
|
import { PlatformModulesProvider } from "../src/platform/ModuleContext";
|
||||||
@@ -14,6 +15,7 @@ import "../src/styles/badges.css";
|
|||||||
import "../src/styles/components.css";
|
import "../src/styles/components.css";
|
||||||
import "../src/styles/dialogs.css";
|
import "../src/styles/dialogs.css";
|
||||||
import "@govoplan/quick-access-webui/styles/quick-access.css";
|
import "@govoplan/quick-access-webui/styles/quick-access.css";
|
||||||
|
import "../../../govoplan-forms-runtime/webui/src/styles/forms-runtime.css";
|
||||||
import "./conformance.css";
|
import "./conformance.css";
|
||||||
|
|
||||||
const theme = new URLSearchParams(window.location.search).get("theme");
|
const theme = new URLSearchParams(window.location.search).get("theme");
|
||||||
@@ -35,9 +37,15 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<PlatformModulesProvider modules={CONFORMANCE_MODULES}>
|
<PlatformModulesProvider modules={CONFORMANCE_MODULES}>
|
||||||
<PlatformLanguageProvider preferredLanguageCode="de">
|
<PlatformLanguageProvider
|
||||||
|
preferredLanguageCode="de"
|
||||||
|
moduleTranslations={[formsRuntimeTranslations]}>
|
||||||
<UnsavedChangesProvider>
|
<UnsavedChangesProvider>
|
||||||
<ConformanceApp />
|
<Routes>
|
||||||
|
<Route path="/forms/public/:publicId" element={<ConformanceApp />} />
|
||||||
|
<Route path="/forms-runtime/:instanceId" element={<ConformanceApp />} />
|
||||||
|
<Route path="*" element={<ConformanceApp />} />
|
||||||
|
</Routes>
|
||||||
</UnsavedChangesProvider>
|
</UnsavedChangesProvider>
|
||||||
</PlatformLanguageProvider>
|
</PlatformLanguageProvider>
|
||||||
</PlatformModulesProvider>
|
</PlatformModulesProvider>
|
||||||
|
|||||||
@@ -19,6 +19,125 @@ async function expectNoAccessibilityViolations(page: import("@playwright/test").
|
|||||||
expect(violations).toEqual([]);
|
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 }) => {
|
test("shared components remain accessible and keyboard operable", async ({ page }) => {
|
||||||
await page.goto("/?theme=light");
|
await page.goto("/?theme=light");
|
||||||
await expect(page.getByRole("heading", { level: 1, name: "Zentrale GovOPlaN-Oberflächen" })).toBeVisible();
|
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 });
|
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) {
|
function quickAccessPayload(includeMessages: boolean) {
|
||||||
const files = {
|
const files = {
|
||||||
id: "files",
|
id: "files",
|
||||||
|
|||||||
Generated
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.40",
|
"version": "0.1.41",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.40",
|
"version": "0.1.41",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
||||||
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
||||||
@@ -378,7 +378,7 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-forms-runtime/webui": {
|
"../../govoplan-forms-runtime/webui": {
|
||||||
"name": "@govoplan/forms-runtime-webui",
|
"name": "@govoplan/forms-runtime-webui",
|
||||||
"version": "0.1.18",
|
"version": "0.1.20",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.18",
|
"@govoplan/core-webui": "^0.1.18",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.40",
|
"version": "0.1.41",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.40",
|
"version": "0.1.41",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.23",
|
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.23",
|
||||||
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.22",
|
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.22",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.40",
|
"version": "0.1.41",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.40",
|
"version": "0.1.41",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ export default function HoverTooltip({
|
|||||||
<span
|
<span
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
className={className}
|
className={className}
|
||||||
|
role={ariaLabel ? "button" : undefined}
|
||||||
tabIndex={triggerTabIndex}
|
tabIndex={triggerTabIndex}
|
||||||
aria-label={translatedAriaLabel}
|
aria-label={translatedAriaLabel}
|
||||||
aria-describedby={isOpen ? tooltipId : undefined}
|
aria-describedby={isOpen ? tooltipId : undefined}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function InlineHelp({ children, className = "" }: InlineHelpProps
|
|||||||
content={children}
|
content={children}
|
||||||
className={`inline-help ${className}`.trim()}
|
className={`inline-help ${className}`.trim()}
|
||||||
ariaLabel="i18n:govoplan-core.show_field_help.e3dfe98f"
|
ariaLabel="i18n:govoplan-core.show_field_help.e3dfe98f"
|
||||||
triggerTabIndex={-1}>
|
triggerTabIndex={0}>
|
||||||
<span className="inline-help-mark" aria-hidden="true">?</span>
|
<span className="inline-help-mark" aria-hidden="true">?</span>
|
||||||
</HoverTooltip>
|
</HoverTooltip>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1276,8 +1276,8 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-core.share.09ca55ca": "Freigabe",
|
"i18n:govoplan-core.share.09ca55ca": "Freigabe",
|
||||||
"i18n:govoplan-core.set_concrete_system_retention_values_blank_day_f.98b9a627": "Set concrete system retention values. Blank day fields mean unlimited retention.",
|
"i18n:govoplan-core.set_concrete_system_retention_values_blank_day_f.98b9a627": "Set concrete system retention values. Blank day fields mean unlimited retention.",
|
||||||
"i18n:govoplan-core.settings.c7f73bb5": "Einstellungen",
|
"i18n:govoplan-core.settings.c7f73bb5": "Einstellungen",
|
||||||
"i18n:govoplan-core.show_content.0528d8d2": "Show content",
|
"i18n:govoplan-core.show_content.0528d8d2": "Inhalt anzeigen",
|
||||||
"i18n:govoplan-core.show_field_help.e3dfe98f": "Show field help",
|
"i18n:govoplan-core.show_field_help.e3dfe98f": "Feldhilfe anzeigen",
|
||||||
"i18n:govoplan-core.show_guided_warnings_while_editing.bc5dba85": "Show guided warnings while editing",
|
"i18n:govoplan-core.show_guided_warnings_while_editing.bc5dba85": "Show guided warnings while editing",
|
||||||
"i18n:govoplan-core.show_header_only.24afefca": "Show header only",
|
"i18n:govoplan-core.show_header_only.24afefca": "Show header only",
|
||||||
"i18n:govoplan-core.show_inline_guidance_and_warnings_while_campaign.a892f5e9": "Show inline guidance and warnings while campaign data is being edited.",
|
"i18n:govoplan-core.show_inline_guidance_and_warnings_while_campaign.a892f5e9": "Show inline guidance and warnings while campaign data is being edited.",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.status-badge { display: inline-flex; align-items: center; height: 24px; border-radius: var(--radius-pill); padding: 0 9px; font-size: 12px; font-weight: 800; background: var(--status-neutral-bg); color: var(--text-soft); text-transform: uppercase; }
|
.status-badge { display: inline-flex; align-items: center; height: 24px; border-radius: var(--radius-pill); padding: 0 9px; font-size: 12px; font-weight: 800; background: var(--status-neutral-bg); color: var(--text-soft); text-transform: uppercase; }
|
||||||
.status-ready, .status-sent, .status-appended, .status-success, .status-active { background: var(--success-soft); color: var(--success-text-strong); }
|
.status-ready, .status-sent, .status-appended, .status-success, .status-active { background: var(--success-soft); color: var(--success-text); }
|
||||||
.status-warning, .status-needs-review, .status-pending { background: var(--warning-soft); color: var(--warning-text-strong); }
|
.status-warning, .status-needs-review, .status-pending { background: var(--warning-soft); color: var(--warning-text-strong); }
|
||||||
.status-blocked, .status-error, .status-danger, .status-failed, .status-failed-permanent { background: var(--danger-bg); color: var(--danger-text-strong); }
|
.status-blocked, .status-error, .status-danger, .status-failed, .status-failed-permanent { background: var(--danger-bg); color: var(--danger-text-strong); }
|
||||||
.status-queued, .status-sending { background: var(--info-soft); color: var(--info-text-strong); }
|
.status-queued, .status-sending { background: var(--info-soft); color: var(--info-text-strong); }
|
||||||
|
|||||||
Reference in New Issue
Block a user