feat: enhance accessibility and testing with data-test attributes and improve error handling
Some checks failed
Workflow / ⚫️ Test (push) Has been cancelled
Workflow / ʦ TypeScript (push) Has been cancelled

This commit is contained in:
T. Zehetbauer
2026-04-01 10:46:44 +02:00
parent 3bcc5c70a3
commit abac22feb1
55 changed files with 1622 additions and 128 deletions

View File

@@ -85,11 +85,11 @@ export function CreateBookingForm({ accountId, account, rooms }: Props) {
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
>
<option value=""> Zimmer wählen </option>
{rooms.map((r) => (
<option key={r.id} value={r.id}>
{r.roomNumber}
{r.name ? ` ${r.name}` : ''} ({r.pricePerNight}{' '}
/Nacht)
{rooms.map((room) => (
<option key={room.id} value={room.id}>
{room.roomNumber}
{room.name ? ` ${room.name}` : ''} (
{room.pricePerNight} /Nacht)
</option>
))}
</select>
@@ -240,10 +240,19 @@ export function CreateBookingForm({ accountId, account, rooms }: Props) {
</Card>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => router.back()}>
<Button
type="button"
variant="outline"
data-test="booking-cancel-btn"
onClick={() => router.back()}
>
Abbrechen
</Button>
<Button type="submit" disabled={isPending}>
<Button
type="submit"
data-test="booking-submit-btn"
disabled={isPending}
>
{isPending ? 'Wird erstellt...' : 'Buchung erstellen'}
</Button>
</div>

View File

@@ -296,10 +296,19 @@ export function CreateCourseForm({ accountId, account }: Props) {
</Card>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => router.back()}>
<Button
type="button"
variant="outline"
onClick={() => router.back()}
data-test="course-cancel-btn"
>
Abbrechen
</Button>
<Button type="submit" disabled={isPending}>
<Button
type="submit"
disabled={isPending}
data-test="course-submit-btn"
>
{isPending ? 'Wird erstellt...' : 'Kurs erstellen'}
</Button>
</div>

View File

@@ -352,10 +352,19 @@ export function CreateEventForm({ accountId, account }: Props) {
</Card>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => router.back()}>
<Button
type="button"
variant="outline"
onClick={() => router.back()}
data-test="event-cancel-btn"
>
Abbrechen
</Button>
<Button type="submit" disabled={isPending}>
<Button
type="submit"
disabled={isPending}
data-test="event-submit-btn"
>
{isPending ? 'Wird erstellt...' : 'Veranstaltung erstellen'}
</Button>
</div>

View File

@@ -66,7 +66,7 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
onSuccess: ({ data }) => {
if (data?.success) {
toast.success('Rechnung erfolgreich erstellt');
router.push(`/home/${account}/finance-cms`);
router.push(`/home/${account}/finance/invoices`);
}
},
onError: ({ error }) => {
@@ -98,7 +98,7 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
<FormItem>
<FormLabel>Rechnungsnummer *</FormLabel>
<FormControl>
<Input {...field} />
<Input data-test="invoice-number-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -111,7 +111,11 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
<FormItem>
<FormLabel>Rechnungsdatum</FormLabel>
<FormControl>
<Input type="date" {...field} />
<Input
type="date"
data-test="invoice-issue-date-input"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -124,7 +128,11 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
<FormItem>
<FormLabel>Fälligkeitsdatum *</FormLabel>
<FormControl>
<Input type="date" {...field} />
<Input
type="date"
data-test="invoice-due-date-input"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -145,7 +153,7 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
<FormItem>
<FormLabel>Name *</FormLabel>
<FormControl>
<Input {...field} />
<Input data-test="invoice-recipient-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -178,6 +186,7 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
type="button"
variant="outline"
size="sm"
data-test="invoice-add-item-btn"
onClick={() =>
append({ description: '', quantity: 1, unitPrice: 0 })
}
@@ -287,6 +296,7 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
min={0}
step="0.5"
className="max-w-[120px]"
data-test="invoice-tax-rate-input"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
@@ -329,10 +339,19 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
</Card>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => router.back()}>
<Button
type="button"
variant="outline"
data-test="invoice-cancel-btn"
onClick={() => router.back()}
>
Abbrechen
</Button>
<Button type="submit" disabled={isPending}>
<Button
type="submit"
data-test="invoice-submit-btn"
disabled={isPending}
>
{isPending ? 'Wird erstellt...' : 'Rechnung erstellen'}
</Button>
</div>

View File

@@ -82,6 +82,7 @@ export function CreateSepaBatchForm({ accountId, account }: Props) {
<FormControl>
<select
{...field}
data-test="sepa-batch-type-select"
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
>
<option value="direct_debit">
@@ -104,6 +105,7 @@ export function CreateSepaBatchForm({ accountId, account }: Props) {
<FormControl>
<Input
placeholder="z.B. Mitgliedsbeiträge Q1 2026"
data-test="sepa-batch-description-input"
{...field}
/>
</FormControl>
@@ -119,7 +121,11 @@ export function CreateSepaBatchForm({ accountId, account }: Props) {
<FormItem>
<FormLabel>Ausführungsdatum *</FormLabel>
<FormControl>
<Input type="date" {...field} />
<Input
type="date"
data-test="sepa-batch-date-input"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -129,10 +135,19 @@ export function CreateSepaBatchForm({ accountId, account }: Props) {
</Card>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => router.back()}>
<Button
type="button"
variant="outline"
data-test="sepa-batch-cancel-btn"
onClick={() => router.back()}
>
Abbrechen
</Button>
<Button type="submit" disabled={isPending}>
<Button
type="submit"
data-test="sepa-batch-submit-btn"
disabled={isPending}
>
{isPending ? 'Wird erstellt...' : 'Einzug erstellen'}
</Button>
</div>

View File

@@ -70,7 +70,7 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
<FormItem>
<FormLabel>Betreff *</FormLabel>
<FormControl>
<Input {...field} />
<Input {...field} data-test="newsletter-subject-input" />
</FormControl>
<FormMessage />
</FormItem>
@@ -88,6 +88,7 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
rows={12}
className="border-input bg-background flex min-h-[200px] w-full rounded-md border px-3 py-2 font-mono text-sm"
placeholder="<h1>Hallo!</h1><p>Ihr Newsletter-Inhalt...</p>"
data-test="newsletter-body-input"
/>
</FormControl>
<FormMessage />
@@ -106,6 +107,7 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
rows={4}
className="border-input bg-background flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm"
placeholder="Nur-Text-Fallback für E-Mail-Clients ohne HTML-Unterstützung"
data-test="newsletter-text-input"
/>
</FormControl>
<FormMessage />
@@ -127,7 +129,11 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
<FormItem>
<FormLabel>Geplanter Versand (optional)</FormLabel>
<FormControl>
<Input type="datetime-local" {...field} />
<Input
type="datetime-local"
{...field}
data-test="newsletter-schedule-input"
/>
</FormControl>
<p className="text-muted-foreground text-xs">
Leer lassen, um den Newsletter als Entwurf zu speichern.
@@ -140,10 +146,19 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
</Card>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => router.back()}>
<Button
type="button"
variant="outline"
onClick={() => router.back()}
data-test="newsletter-cancel-btn"
>
Abbrechen
</Button>
<Button type="submit" disabled={isPending}>
<Button
type="submit"
disabled={isPending}
data-test="newsletter-submit-btn"
>
{isPending ? 'Wird erstellt...' : 'Newsletter erstellen'}
</Button>
</div>

View File

@@ -148,10 +148,19 @@ export function CreatePageForm({ accountId, account }: Props) {
</CardContent>
</Card>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => router.back()}>
<Button
type="button"
variant="outline"
onClick={() => router.back()}
data-test="page-cancel-btn"
>
Abbrechen
</Button>
<Button type="submit" disabled={isPending}>
<Button
type="submit"
disabled={isPending}
data-test="page-submit-btn"
>
{isPending ? 'Wird erstellt...' : 'Seite erstellen & Editor öffnen'}
</Button>
</div>

View File

@@ -159,10 +159,19 @@ export function CreatePostForm({ accountId, account }: Props) {
</CardContent>
</Card>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => router.back()}>
<Button
type="button"
variant="outline"
onClick={() => router.back()}
data-test="post-cancel-btn"
>
Abbrechen
</Button>
<Button type="submit" disabled={isPending}>
<Button
type="submit"
disabled={isPending}
data-test="post-submit-btn"
>
{isPending ? 'Wird erstellt...' : 'Beitrag erstellen'}
</Button>
</div>

View File

@@ -465,15 +465,17 @@ const EventListBlock = ({
<h2 className="mb-6 text-2xl font-bold">Veranstaltungen</h2>
<div className="space-y-3">
{items.map((event) => {
const d = new Date(event.event_date);
const eventDate = new Date(event.event_date);
const isExpanded = expandedId === event.id;
const isSuccess = successId === event.id;
return (
<div key={event.id} className="overflow-hidden rounded-lg border">
<div className="flex items-center gap-4 p-4">
<div className="bg-primary/10 text-primary flex h-14 w-14 shrink-0 flex-col items-center justify-center rounded-lg">
<span className="text-lg font-bold">{d.getDate()}</span>
<span className="text-xs">{formatMonthShort(d)}</span>
<span className="text-lg font-bold">
{eventDate.getDate()}
</span>
<span className="text-xs">{formatMonthShort(eventDate)}</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="font-semibold">{event.name}</h3>

View File

@@ -83,6 +83,7 @@ export function CreateProtocolForm({
<FormLabel>Titel *</FormLabel>
<FormControl>
<Input
data-test="protocol-title-input"
placeholder="z.B. Vorstandssitzung März 2026"
{...field}
/>
@@ -100,7 +101,11 @@ export function CreateProtocolForm({
<FormItem>
<FormLabel>Sitzungsdatum *</FormLabel>
<FormControl>
<Input type="date" {...field} />
<Input
data-test="protocol-date-input"
type="date"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -115,6 +120,7 @@ export function CreateProtocolForm({
<FormLabel>Sitzungsart *</FormLabel>
<FormControl>
<select
data-test="protocol-type-select"
{...field}
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
>
@@ -136,7 +142,11 @@ export function CreateProtocolForm({
<FormItem>
<FormLabel>Ort</FormLabel>
<FormControl>
<Input placeholder="z.B. Vereinsheim" {...field} />
<Input
data-test="protocol-location-input"
placeholder="z.B. Vereinsheim"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -223,10 +233,19 @@ export function CreateProtocolForm({
{/* Submit */}
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => router.back()}>
<Button
type="button"
variant="outline"
data-test="protocol-cancel-btn"
onClick={() => router.back()}
>
Abbrechen
</Button>
<Button type="submit" disabled={isPending}>
<Button
type="submit"
disabled={isPending}
data-test="protocol-submit-btn"
>
{isPending ? 'Wird gespeichert...' : 'Protokoll erstellen'}
</Button>
</div>

View File

@@ -2,6 +2,7 @@
import Link from 'next/link';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
interface MeetingsTabNavigationProps {
@@ -10,9 +11,9 @@ interface MeetingsTabNavigationProps {
}
const TABS = [
{ id: 'overview', label: 'Übersicht', path: '' },
{ id: 'protocols', label: 'Protokolle', path: '/protocols' },
{ id: 'tasks', label: 'Offene Aufgaben', path: '/tasks' },
{ id: 'overview', i18nKey: 'meetings:nav.overview', path: '' },
{ id: 'protocols', i18nKey: 'meetings:nav.protocols', path: '/protocols' },
{ id: 'tasks', i18nKey: 'meetings:nav.tasks', path: '/tasks' },
] as const;
export function MeetingsTabNavigation({
@@ -25,7 +26,7 @@ export function MeetingsTabNavigation({
<div className="mb-6 border-b">
<nav
className="-mb-px flex space-x-1 overflow-x-auto"
aria-label="Sitzungsprotokolle Navigation"
aria-label="Meeting Protocols Navigation"
>
{TABS.map((tab) => {
const isActive = tab.id === activeTab;
@@ -34,6 +35,7 @@ export function MeetingsTabNavigation({
<Link
key={tab.id}
href={`${basePath}${tab.path}`}
data-test={`meetings-tab-${tab.id}`}
className={cn(
'border-b-2 px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors',
isActive
@@ -41,7 +43,7 @@ export function MeetingsTabNavigation({
: 'text-muted-foreground hover:border-muted-foreground/30 hover:text-foreground border-transparent',
)}
>
{tab.label}
<Trans i18nKey={tab.i18nKey} />
</Link>
);
})}

View File

@@ -174,6 +174,7 @@ export function OpenTasksView({
</p>
<div className="flex gap-2">
<Button
data-test="tasks-prev-btn"
variant="outline"
size="sm"
disabled={page <= 1}
@@ -182,6 +183,7 @@ export function OpenTasksView({
Zurück
</Button>
<Button
data-test="tasks-next-btn"
variant="outline"
size="sm"
disabled={page >= totalPages}

View File

@@ -141,6 +141,7 @@ export function ProtocolItemsList({
</td>
<td className="p-3 text-center">
<Badge
data-test="item-status-toggle"
variant={
(ITEM_STATUS_COLORS[item.status] as
| 'default'
@@ -156,6 +157,7 @@ export function ProtocolItemsList({
</td>
<td className="p-3 text-right">
<Button
data-test="item-delete-btn"
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive"

View File

@@ -69,16 +69,16 @@ export function ProtocolsDataTable({
);
const handleSearch = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
(formEvent: React.FormEvent) => {
formEvent.preventDefault();
updateParams({ q: form.getValues('search') });
},
[form, updateParams],
);
const handleTypeChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
updateParams({ type: e.target.value });
(changeEvent: React.ChangeEvent<HTMLSelectElement>) => {
updateParams({ type: changeEvent.target.value });
},
[updateParams],
);
@@ -96,17 +96,24 @@ export function ProtocolsDataTable({
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<form onSubmit={handleSearch} className="flex gap-2">
<Input
data-test="protocols-search-input"
placeholder="Protokoll suchen..."
className="w-64"
{...form.register('search')}
/>
<Button type="submit" variant="outline" size="sm">
<Button
type="submit"
variant="outline"
size="sm"
data-test="protocols-search-btn"
>
Suchen
</Button>
</form>
<div className="flex items-center gap-3">
<select
data-test="protocols-type-filter"
value={currentType}
onChange={handleTypeChange}
className="border-input bg-background flex h-9 rounded-md border px-3 py-1 text-sm shadow-sm"
@@ -119,7 +126,7 @@ export function ProtocolsDataTable({
</select>
<Link href={`/home/${account}/meetings/protocols/new`}>
<Button size="sm">
<Button size="sm" data-test="protocols-new-btn">
<Plus className="mr-2 h-4 w-4" />
Neues Protokoll
</Button>