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:
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user