feat(C): Phase C — Core UI prüfen, vervollständigen, testen

C-ERR-BOUNDARY: ErrorBoundary erweitert (trace_id, Fallback-UI, PluginErrorBoundary)
C-NOTIF: NotificationDropdown mit System-Channel-Link
C-DOCS: ApiDocs.tsx Seite (Swagger UI iframe)
C-A11Y: aria-label, min-h-touch, focus-visible ergänzt
C-FE-TEST: 29 neue Tests (ErrorBoundary, ApiDocs, PrintButton, themeStore, NotificationDropdown)
C-DOC: ui-design-guidelines.md aktualisiert

Verifiziert (keine Änderungen nötig):
- C-TAGS, C-CF, C-FILTER, C-DEDUP, C-ONBOARD, C-THEME, C-PWA, C-PRINT

TSC: 0 errors, Build: erfolgreich, Vitest: 29/29 passed
This commit is contained in:
Agent Zero
2026-08-13 21:57:59 +02:00
parent 0e72d4624d
commit 25b2581653
15 changed files with 713 additions and 62 deletions
@@ -0,0 +1,108 @@
/**
* ErrorBoundary tests — verifies error catching, trace_id display, and retry.
*/
import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
// Component that throws on render
function ThrowOnRender({ message }: { message: string }) {
throw new Error(message);
}
// Component that toggles throwing
function ConditionalThrow({ shouldThrow }: { shouldThrow: boolean }) {
if (shouldThrow) throw new Error('Conditional error');
return <div data-testid="safe-content">Safe content</div>;
}
describe('ErrorBoundary', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(console, 'error').mockImplementation(() => {});
});
it('renders children when no error occurs', () => {
render(
<ErrorBoundary>
<div data-testid="child">Hello World</div>
</ErrorBoundary>
);
expect(screen.getByTestId('child')).toBeInTheDocument();
});
it('catches render errors and shows fallback UI', () => {
render(
<ErrorBoundary>
<ThrowOnRender message="Test error" />
</ErrorBoundary>
);
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByText('Etwas ist schiefgelaufen')).toBeInTheDocument();
});
it('displays a trace_id when error occurs', () => {
render(
<ErrorBoundary>
<ThrowOnRender message="Trace test" />
</ErrorBoundary>
);
const traceEl = screen.getByTestId('error-trace-id');
expect(traceEl).toBeInTheDocument();
expect(traceEl.textContent).toMatch(/Trace-ID: err-/);
});
it('shows error details in a details element', () => {
render(
<ErrorBoundary>
<ThrowOnRender message="Detailed error" />
</ErrorBoundary>
);
expect(screen.getByText('Fehlerdetails')).toBeInTheDocument();
});
it('has retry and reload buttons', () => {
render(
<ErrorBoundary>
<ThrowOnRender message="Button test" />
</ErrorBoundary>
);
expect(screen.getByText('Erneut versuchen')).toBeInTheDocument();
expect(screen.getByText('Neu laden')).toBeInTheDocument();
});
it('retries and resets error state when retry is clicked', () => {
let shouldThrow = true;
function Wrapper() {
if (shouldThrow) throw new Error('Retry test error');
return <div data-testid="safe-content">Safe content</div>;
}
const { rerender } = render(
<ErrorBoundary>
<Wrapper />
</ErrorBoundary>
);
expect(screen.getByRole('alert')).toBeInTheDocument();
// Stop throwing, then click retry — ErrorBoundary re-renders children without error
shouldThrow = false;
fireEvent.click(screen.getByText('Erneut versuchen'));
expect(screen.getByTestId('safe-content')).toBeInTheDocument();
});
it('uses custom fallback when provided', () => {
const customFallback = vi.fn(() => <div data-testid="custom-fallback">Custom</div>);
render(
<ErrorBoundary fallback={customFallback}>
<ThrowOnRender message="Custom fallback" />
</ErrorBoundary>
);
expect(screen.getByTestId('custom-fallback')).toBeInTheDocument();
expect(customFallback).toHaveBeenCalled();
});
});
@@ -0,0 +1,62 @@
/**
* PrintButton tests — verifies dropdown, print and PDF export actions.
*/
import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { PrintButton } from '@/components/common/PrintButton';
// Mock print utilities
vi.mock('@/utils/print', () => ({
printElement: vi.fn(),
printCurrentPage: vi.fn(),
exportToPDF: vi.fn(),
}));
import { printElement, printCurrentPage, exportToPDF } from '@/utils/print';
describe('PrintButton', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renders the trigger button', () => {
render(<PrintButton />);
expect(screen.getByTestId('print-button-trigger')).toBeInTheDocument();
});
it('opens dropdown on click', () => {
render(<PrintButton />);
fireEvent.click(screen.getByTestId('print-button-trigger'));
expect(screen.getByTestId('print-button-dropdown')).toBeInTheDocument();
});
it('calls printElement when targetId is set and print is clicked', () => {
render(<PrintButton targetId="my-section" />);
fireEvent.click(screen.getByTestId('print-button-trigger'));
fireEvent.click(screen.getByTestId('print-button-print'));
expect(printElement).toHaveBeenCalledWith('my-section');
});
it('calls printCurrentPage when no targetId and print is clicked', () => {
render(<PrintButton />);
fireEvent.click(screen.getByTestId('print-button-trigger'));
fireEvent.click(screen.getByTestId('print-button-print'));
expect(printCurrentPage).toHaveBeenCalled();
});
it('calls exportToPDF with targetId and filename', () => {
render(<PrintButton targetId="report" filename="monthly-report" />);
fireEvent.click(screen.getByTestId('print-button-trigger'));
fireEvent.click(screen.getByTestId('print-button-pdf'));
expect(exportToPDF).toHaveBeenCalledWith('report', 'monthly-report');
});
it('has aria-haspopup and aria-expanded attributes', () => {
render(<PrintButton />);
const btn = screen.getByTestId('print-button-trigger');
expect(btn).toHaveAttribute('aria-haspopup', 'menu');
expect(btn).toHaveAttribute('aria-expanded', 'false');
});
});