T07a: frontend core SPA — shell + auth + routing + i18n + UI library + a11y

- React 18 + Vite + TypeScript + Tailwind CSS setup
- AppShell with Sidebar (plugin menu) + TopBar (tenant switcher, search, notifications, user menu)
- Auth pages: Login, PasswordResetRequest, PasswordResetConfirm
- Protected routes with auth guard
- API client (axios with interceptors: session cookie, 401 redirect, 422 validation)
- TanStack Query hooks for auth, users, companies, contacts, notifications
- Zustand stores: authStore, uiStore
- i18n setup (de/en locales) with react-i18next
- UI component library: Button, Input, Select, Modal, Toast, Table, Card, Badge, Avatar, Pagination, EmptyState, Skeleton, ConfirmDialog
- Accessibility: ARIA labels, 44px touch targets, keyboard nav, reduced-motion, sr-only
- Design tokens from prototype as CSS custom properties
- 111 tests passing across 20 test files
- tsc --noEmit: 0 errors
- npm run build: success (471KB JS, 24KB CSS)
This commit is contained in:
leocrm-bot
2026-06-29 07:55:47 +02:00
parent f8193a6ab5
commit 22976abe92
66 changed files with 8598 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
import React from 'react';
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Avatar } from '@/components/ui/Avatar';
describe('Avatar', () => {
it('renders initials when no src provided', () => {
render(<Avatar name="Max Mustermann" />);
expect(screen.getByText('MM')).toBeInTheDocument();
});
it('renders single initial for single name', () => {
render(<Avatar name="Anna" />);
expect(screen.getByText('A')).toBeInTheDocument();
});
it('has role=img and aria-label', () => {
render(<Avatar name="Max Mustermann" />);
expect(screen.getByRole('img')).toHaveAttribute('aria-label', 'Avatar von Max Mustermann');
});
it('renders image when src provided', () => {
render(<Avatar name="Test" src="https://example.com/avatar.png" />);
const img = screen.getByRole('img').querySelector('img')!;
expect(img).not.toBeNull();
});
it('falls back to initials on image error', () => {
render(<Avatar name="Max Mustermann" src="https://example.com/broken.png" />);
const img = screen.getByRole('img').querySelector('img')!;
fireEvent.error(img);
expect(screen.getByText('MM')).toBeInTheDocument();
});
});
+33
View File
@@ -0,0 +1,33 @@
import React from 'react';
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Badge } from '@/components/ui/Badge';
describe('Badge', () => {
it('renders children text', () => {
render(<Badge>aktiv</Badge>);
expect(screen.getByText('aktiv')).toBeInTheDocument();
});
it('renders with default variant', () => {
render(<Badge>Standard</Badge>);
const badge = screen.getByText('Standard');
expect(badge.className).toContain('bg-secondary-100');
});
it('renders with success variant', () => {
render(<Badge variant="success">OK</Badge>);
expect(screen.getByText('OK').className).toContain('bg-success-100');
});
it('renders with danger variant', () => {
render(<Badge variant="danger">Fehler</Badge>);
expect(screen.getByText('Fehler').className).toContain('bg-danger-100');
});
it('renders dot indicator when dot=true', () => {
const { container } = render(<Badge dot>aktiv</Badge>);
const dot = container.querySelector('span > span');
expect(dot).toBeInTheDocument();
});
});
+61
View File
@@ -0,0 +1,61 @@
import React from 'react';
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '@/components/ui/Button';
describe('Button', () => {
it('renders primary variant by default', () => {
render(<Button>Klick mich</Button>);
const btn = screen.getByRole('button', { name: 'Klick mich' });
expect(btn).toBeInTheDocument();
expect(btn.className).toContain('bg-primary-600');
});
it('renders secondary variant', () => {
render(<Button variant="secondary">Sekundär</Button>);
const btn = screen.getByRole('button', { name: 'Sekundär' });
expect(btn.className).toContain('bg-secondary-200');
});
it('renders danger variant', () => {
render(<Button variant="danger">Löschen</Button>);
const btn = screen.getByRole('button', { name: 'Löschen' });
expect(btn.className).toContain('bg-danger-600');
});
it('renders ghost variant', () => {
render(<Button variant="ghost">Geist</Button>);
const btn = screen.getByRole('button', { name: 'Geist' });
expect(btn.className).toContain('bg-transparent');
});
it('calls onClick when clicked', async () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Klick</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
it('shows loading spinner when isLoading', () => {
render(<Button isLoading>Laden</Button>);
const btn = screen.getByRole('button');
expect(btn).toBeDisabled();
expect(btn).toHaveAttribute('aria-busy', 'true');
});
it('has min-h-touch for 44px touch target', () => {
render(<Button>Test</Button>);
expect(screen.getByRole('button').className).toContain('min-h-touch');
});
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Disabled</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
it('renders with full width when fullWidth', () => {
render(<Button fullWidth>Voll</Button>);
expect(screen.getByRole('button').className).toContain('w-full');
});
});
+26
View File
@@ -0,0 +1,26 @@
import React from 'react';
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Card } from '@/components/ui/Card';
describe('Card', () => {
it('renders children content', () => {
render(<Card>Inhalt der Karte</Card>);
expect(screen.getByText('Inhalt der Karte')).toBeInTheDocument();
});
it('renders title when provided', () => {
render(<Card title="Firmendetails">Inhalt</Card>);
expect(screen.getByText('Firmendetails')).toBeInTheDocument();
});
it('renders description when provided', () => {
render(<Card title="Test" description="Eine Beschreibung">Inhalt</Card>);
expect(screen.getByText('Eine Beschreibung')).toBeInTheDocument();
});
it('renders footer when provided', () => {
render(<Card footer={<button>Aktion</button>}>Inhalt</Card>);
expect(screen.getByRole('button', { name: 'Aktion' })).toBeInTheDocument();
});
});
@@ -0,0 +1,46 @@
import React from 'react';
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
describe('ConfirmDialog', () => {
it('renders nothing when open=false', () => {
render(<ConfirmDialog open={false} message="Wirklich löschen?" onConfirm={() => {}} onCancel={() => {}} />);
expect(screen.queryByText('Wirklich löschen?')).not.toBeInTheDocument();
});
it('renders message when open=true', () => {
render(<ConfirmDialog open={true} message="Wirklich löschen?" onConfirm={() => {}} onCancel={() => {}} />);
expect(screen.getByText('Wirklich löschen?')).toBeInTheDocument();
});
it('calls onConfirm when confirm button clicked', () => {
const onConfirm = vi.fn();
render(<ConfirmDialog open={true} message="Löschen?" onConfirm={onConfirm} onCancel={() => {}} />);
fireEvent.click(screen.getByRole('button', { name: 'Bestätigen' }));
expect(onConfirm).toHaveBeenCalledTimes(1);
});
it('calls onCancel when cancel button clicked', () => {
const onCancel = vi.fn();
render(<ConfirmDialog open={true} message="Löschen?" onConfirm={() => {}} onCancel={onCancel} />);
fireEvent.click(screen.getByRole('button', { name: 'Abbrechen' }));
expect(onCancel).toHaveBeenCalledTimes(1);
});
it('renders default title', () => {
render(<ConfirmDialog open={true} message="Test" onConfirm={() => {}} onCancel={() => {}} />);
expect(screen.getByText('Bestätigung erforderlich')).toBeInTheDocument();
});
it('renders custom title', () => {
render(<ConfirmDialog open={true} title="Löschen bestätigen" message="Test" onConfirm={() => {}} onCancel={() => {}} />);
expect(screen.getByText('Löschen bestätigen')).toBeInTheDocument();
});
it('uses danger variant button when variant=danger', () => {
render(<ConfirmDialog open={true} variant="danger" message="Test" onConfirm={() => {}} onCancel={() => {}} />);
const confirmBtn = screen.getByRole('button', { name: 'Bestätigen' });
expect(confirmBtn.className).toContain('bg-danger-600');
});
});
@@ -0,0 +1,28 @@
import React from 'react';
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { EmptyState } from '@/components/ui/EmptyState';
describe('EmptyState', () => {
it('renders default title and description', () => {
render(<EmptyState />);
expect(screen.getByText('Nichts gefunden')).toBeInTheDocument();
expect(screen.getByText('Es sind noch keine Daten vorhanden.')).toBeInTheDocument();
});
it('renders custom title and description', () => {
render(<EmptyState title="Keine Firmen" description="Erstellen Sie Ihre erste Firma." />);
expect(screen.getByText('Keine Firmen')).toBeInTheDocument();
expect(screen.getByText('Erstellen Sie Ihre erste Firma.')).toBeInTheDocument();
});
it('renders action when provided', () => {
render(<EmptyState action={<button>Firma erstellen</button>} />);
expect(screen.getByRole('button', { name: 'Firma erstellen' })).toBeInTheDocument();
});
it('has role=status', () => {
render(<EmptyState />);
expect(screen.getByRole('status')).toBeInTheDocument();
});
});
+49
View File
@@ -0,0 +1,49 @@
import React from 'react';
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Input } from '@/components/ui/Input';
describe('Input', () => {
it('renders with label', () => {
render(<Input label="Name" />);
expect(screen.getByText('Name')).toBeInTheDocument();
});
it('renders error message with role=alert', () => {
render(<Input label="Email" error="Ungültige E-Mail" />);
expect(screen.getByRole('alert')).toHaveTextContent('Ungültige E-Mail');
});
it('renders helper text', () => {
render(<Input label="Passwort" helperText="Mindestens 8 Zeichen" />);
expect(screen.getByText('Mindestens 8 Zeichen')).toBeInTheDocument();
});
it('does not render helper text when error is present', () => {
render(<Input label="Passwort" error="Fehler" helperText="Hilfetext" />);
expect(screen.queryByText('Hilfetext')).not.toBeInTheDocument();
});
it('marks input as required with aria-required', () => {
render(<Input label="Name" required />);
expect(screen.getByLabelText(/Name/)).toHaveAttribute('aria-required', 'true');
});
it('sets aria-invalid when error present', () => {
render(<Input label="Email" error="Fehler" />);
expect(screen.getByLabelText(/Email/)).toHaveAttribute('aria-invalid', 'true');
});
it('has min-h-touch for 44px target', () => {
render(<Input label="Test" />);
expect(screen.getByLabelText(/Test/).className).toContain('min-h-touch');
});
it('links error via aria-describedby', () => {
render(<Input label="Email" error="Fehler" />);
const input = screen.getByLabelText(/Email/);
const errorId = input.getAttribute('aria-describedby');
expect(errorId).toBeTruthy();
expect(document.getElementById(errorId!)).toHaveTextContent('Fehler');
});
});
+54
View File
@@ -0,0 +1,54 @@
import React from 'react';
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Modal } from '@/components/ui/Modal';
describe('Modal', () => {
it('renders nothing when open=false', () => {
render(<Modal open={false} onClose={() => {}}>Content</Modal>);
expect(screen.queryByText('Content')).not.toBeInTheDocument();
});
it('renders content when open=true', () => {
render(<Modal open={true} onClose={() => {}}>Modal Content</Modal>);
expect(screen.getByText('Modal Content')).toBeInTheDocument();
});
it('has role=dialog and aria-modal=true', () => {
render(<Modal open={true} onClose={() => {}}>Content</Modal>);
expect(screen.getByRole('dialog')).toHaveAttribute('aria-modal', 'true');
});
it('calls onClose when backdrop clicked', () => {
const onClose = vi.fn();
render(<Modal open={true} onClose={onClose}>Content</Modal>);
fireEvent.click(screen.getByTestId('modal-backdrop'));
expect(onClose).toHaveBeenCalledTimes(1);
});
it('calls onClose when ESC pressed', () => {
const onClose = vi.fn();
render(<Modal open={true} onClose={onClose}>Content</Modal>);
fireEvent.keyDown(document, { key: 'Escape' });
expect(onClose).toHaveBeenCalledTimes(1);
});
it('does not call onClose on backdrop when closeOnBackdrop=false', () => {
const onClose = vi.fn();
render(<Modal open={true} onClose={onClose} closeOnBackdrop={false}>Content</Modal>);
fireEvent.click(screen.getByTestId('modal-backdrop'));
expect(onClose).not.toHaveBeenCalled();
});
it('renders title with aria-labelledby', () => {
render(<Modal open={true} onClose={() => {}} title="Test Title">Content</Modal>);
const dialog = screen.getByRole('dialog');
expect(dialog).toHaveAttribute('aria-labelledby', 'modal-title');
expect(screen.getByText('Test Title')).toBeInTheDocument();
});
it('renders close button with aria-label', () => {
render(<Modal open={true} onClose={() => {}}>Content</Modal>);
expect(screen.getByLabelText('Close dialog')).toBeInTheDocument();
});
});
@@ -0,0 +1,61 @@
import React from 'react';
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Pagination } from '@/components/ui/Pagination';
describe('Pagination', () => {
it('renders page info and navigation', () => {
render(
<Pagination currentPage={2} totalPages={5} total={100} pageSize={20} onPageChange={() => {}} />
);
expect(screen.getByText('2140')).toBeInTheDocument();
expect(screen.getByText('100')).toBeInTheDocument();
});
it('calls onPageChange with next page', () => {
const onPageChange = vi.fn();
render(
<Pagination currentPage={1} totalPages={3} total={60} pageSize={20} onPageChange={onPageChange} />
);
fireEvent.click(screen.getByLabelText('Weiter'));
expect(onPageChange).toHaveBeenCalledWith(2);
});
it('calls onPageChange with previous page', () => {
const onPageChange = vi.fn();
render(
<Pagination currentPage={2} totalPages={3} total={60} pageSize={20} onPageChange={onPageChange} />
);
fireEvent.click(screen.getByLabelText('Zurück'));
expect(onPageChange).toHaveBeenCalledWith(1);
});
it('disables previous on first page', () => {
render(
<Pagination currentPage={1} totalPages={3} total={60} pageSize={20} onPageChange={() => {}} />
);
expect(screen.getByLabelText('Zurück')).toBeDisabled();
});
it('disables next on last page', () => {
render(
<Pagination currentPage={3} totalPages={3} total={60} pageSize={20} onPageChange={() => {}} />
);
expect(screen.getByLabelText('Weiter')).toBeDisabled();
});
it('returns null when totalPages <= 1', () => {
const { container } = render(
<Pagination currentPage={1} totalPages={1} total={5} pageSize={20} onPageChange={() => {}} />
);
expect(container.firstChild).toBeNull();
});
it('has aria-current on current page button', () => {
render(
<Pagination currentPage={2} totalPages={5} total={100} pageSize={20} onPageChange={() => {}} />
);
const currentBtn = screen.getByLabelText('Seite 2');
expect(currentBtn).toHaveAttribute('aria-current', 'page');
});
});
+45
View File
@@ -0,0 +1,45 @@
import React from 'react';
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Select } from '@/components/ui/Select';
const options = [
{ value: 'de', label: 'Deutsch' },
{ value: 'en', label: 'English' },
];
describe('Select', () => {
it('renders label and options', () => {
render(<Select label="Sprache" options={options} />);
expect(screen.getByText('Sprache')).toBeInTheDocument();
expect(screen.getByText('Deutsch')).toBeInTheDocument();
expect(screen.getByText('English')).toBeInTheDocument();
});
it('renders error with role=alert', () => {
render(<Select label="Sprache" options={options} error="Bitte auswählen" />);
expect(screen.getByRole('alert')).toHaveTextContent('Bitte auswählen');
});
it('renders helper text', () => {
render(<Select label="Sprache" options={options} helperText="Wählen Sie eine Sprache" />);
expect(screen.getByText('Wählen Sie eine Sprache')).toBeInTheDocument();
});
it('calls onChange when value changes', () => {
const onChange = vi.fn();
render(<Select label="Sprache" options={options} onChange={onChange} />);
fireEvent.change(screen.getByLabelText(/Sprache/), { target: { value: 'en' } });
expect(onChange).toHaveBeenCalled();
});
it('has min-h-touch for 44px target', () => {
render(<Select label="Sprache" options={options} />);
expect(screen.getByLabelText(/Sprache/).className).toContain('min-h-touch');
});
it('sets aria-required when required', () => {
render(<Select label="Sprache" options={options} required />);
expect(screen.getByLabelText(/Sprache/)).toHaveAttribute('aria-required', 'true');
});
});
@@ -0,0 +1,41 @@
import React from 'react';
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Skeleton, SkeletonText } from '@/components/ui/Skeleton';
describe('Skeleton', () => {
it('renders with role=status', () => {
render(<Skeleton />);
expect(screen.getByRole('status')).toBeInTheDocument();
});
it('renders with aria-label', () => {
render(<Skeleton />);
expect(screen.getByRole('status')).toHaveAttribute('aria-label', 'Wird geladen');
});
it('applies variant classes correctly', () => {
const { container } = render(<Skeleton variant="circle" />);
expect(container.firstChild).toHaveClass('rounded-full');
});
it('applies width and height styles', () => {
const { container } = render(<Skeleton width="200px" height="50px" />);
const el = container.firstChild as HTMLElement;
expect(el.style.width).toBe('200px');
expect(el.style.height).toBe('50px');
});
});
describe('SkeletonText', () => {
it('renders specified number of lines', () => {
const { container } = render(<SkeletonText lines={3} />);
const skeletons = container.querySelectorAll('[role="status"]');
expect(skeletons.length).toBeGreaterThanOrEqual(1);
});
it('has role=status for accessibility', () => {
render(<SkeletonText lines={2} />);
expect(screen.getAllByRole('status').length).toBeGreaterThan(0);
});
});
+72
View File
@@ -0,0 +1,72 @@
import React from 'react';
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Table, TableColumn } from '@/components/ui/Table';
interface TestRow {
id: string;
name: string;
email: string;
}
const columns: TableColumn<TestRow>[] = [
{ key: 'name', header: 'Name', sortable: true },
{ key: 'email', header: 'E-Mail', sortable: true },
{ key: 'id', header: 'ID', sortable: false },
];
const data: TestRow[] = [
{ id: '1', name: 'Anna Schmidt', email: 'anna@test.de' },
{ id: '2', name: 'Max Müller', email: 'max@test.de' },
{ id: '3', name: 'Lisa Weber', email: 'lisa@test.de' },
];
describe('Table', () => {
it('renders all rows and headers', () => {
render(<Table columns={columns} data={data} rowKey={(r) => r.id} />);
expect(screen.getByText('Anna Schmidt')).toBeInTheDocument();
expect(screen.getByText('Max Müller')).toBeInTheDocument();
expect(screen.getByText('Lisa Weber')).toBeInTheDocument();
expect(screen.getByText('Name')).toBeInTheDocument();
expect(screen.getByText('E-Mail')).toBeInTheDocument();
});
it('sortable headers have role=button and aria-label', () => {
render(<Table columns={columns} data={data} rowKey={(r) => r.id} />);
const nameHeader = screen.getByRole('button', { name: /Name/ });
expect(nameHeader).toBeInTheDocument();
});
it('clicking sortable header sorts ascending then descending', () => {
render(<Table columns={columns} data={data} rowKey={(r) => r.id} />);
const nameHeader = screen.getByRole('button', { name: /Name/ });
fireEvent.click(nameHeader);
// ascending: Anna, Lisa, Max
const rows = screen.getAllByRole('row');
expect(rows[1]).toHaveTextContent('Anna Schmidt');
expect(rows[3]).toHaveTextContent('Max Müller');
fireEvent.click(nameHeader);
// descending: Max, Lisa, Anna
const rowsDesc = screen.getAllByRole('row');
expect(rowsDesc[1]).toHaveTextContent('Max Müller');
expect(rowsDesc[3]).toHaveTextContent('Anna Schmidt');
});
it('aria-sort updates on sort', () => {
render(<Table columns={columns} data={data} rowKey={(r) => r.id} />);
const nameHeader = screen.getByRole('button', { name: /Name/ });
expect(nameHeader.closest('th')).toHaveAttribute('aria-sort', 'none');
fireEvent.click(nameHeader);
expect(nameHeader.closest('th')).toHaveAttribute('aria-sort', 'ascending');
});
it('shows empty message when no data', () => {
render(<Table columns={columns} data={[]} rowKey={(r) => r.id} emptyMessage="Keine Daten" />);
expect(screen.getByText('Keine Daten')).toBeInTheDocument();
});
it('shows loading state', () => {
render(<Table columns={columns} data={[]} rowKey={(r) => r.id} loading={true} />);
expect(screen.getByText('Wird geladen...')).toBeInTheDocument();
});
});
+56
View File
@@ -0,0 +1,56 @@
import React from 'react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, act, fireEvent } from '@testing-library/react';
import { ToastContainer, useToast } from '@/components/ui/Toast';
import { useUIStore } from '@/store/uiStore';
describe('Toast', () => {
beforeEach(() => {
useUIStore.setState({ toasts: [] });
});
it('renders toast container', () => {
render(<ToastContainer />);
expect(screen.getByTestId('toast-container')).toBeInTheDocument();
});
it('displays toast when added to store', () => {
useUIStore.setState({
toasts: [{ id: '1', type: 'success', message: 'Erfolgreich gespeichert' }],
});
render(<ToastContainer />);
expect(screen.getByText('Erfolgreich gespeichert')).toBeInTheDocument();
});
it('toast has role=alert for accessibility', () => {
useUIStore.setState({
toasts: [{ id: '1', type: 'error', message: 'Fehler aufgetreten' }],
});
render(<ToastContainer />);
expect(screen.getByRole('alert')).toHaveTextContent('Fehler aufgetreten');
});
it('auto-dismisses after duration', () => {
vi.useFakeTimers();
useUIStore.setState({
toasts: [{ id: '1', type: 'info', message: 'Info', duration: 1000 }],
});
render(<ToastContainer />);
expect(screen.getByText('Info')).toBeInTheDocument();
act(() => {
vi.advanceTimersByTime(1000);
});
expect(screen.queryByText('Info')).not.toBeInTheDocument();
vi.useRealTimers();
});
it('can be manually dismissed', () => {
useUIStore.setState({
toasts: [{ id: '1', type: 'warning', message: 'Warnung' }],
});
render(<ToastContainer />);
const closeBtn = screen.getByLabelText('Close notification');
fireEvent.click(closeBtn);
expect(screen.queryByText('Warnung')).not.toBeInTheDocument();
});
});
+35
View File
@@ -0,0 +1,35 @@
import { describe, it, expect, beforeAll } from 'vitest';
import i18n from '@/i18n';
describe('i18n', () => {
beforeAll(async () => {
await i18n.changeLanguage('de');
});
it('loads German locale by default', () => {
expect(i18n.language).toBe('de');
});
it('translates key in German', () => {
expect(i18n.t('nav.dashboard')).toBe('Dashboard');
expect(i18n.t('nav.companies')).toBe('Firmen');
expect(i18n.t('auth.login')).toBe('Anmelden');
});
it('can switch to English', async () => {
await i18n.changeLanguage('en');
expect(i18n.language).toBe('en');
expect(i18n.t('auth.login')).toBe('Sign In');
expect(i18n.t('nav.companies')).toBe('Companies');
});
it('falls back to German for missing English keys', async () => {
await i18n.changeLanguage('en');
expect(i18n.t('app.name')).toBe('leocrm');
});
it('switches back to German', async () => {
await i18n.changeLanguage('de');
expect(i18n.t('auth.login')).toBe('Anmelden');
});
});