/** * RiskHeatmap Component Tests * * Tests fuer die erweiterte Risiko-Matrix-Visualisierung */ import { describe, it, expect, vi } from 'vitest' import { render, screen, fireEvent } from '@testing-library/react' import RiskHeatmap, { calculateRiskLevel, MiniRiskMatrix, RiskDistribution, type Risk, type Control, } from '../../components/compliance/charts/RiskHeatmap' // Mock-Daten const mockRisks: Risk[] = [ { id: '1', risk_id: 'RISK-001', title: 'Data Breach Risk', description: 'Risiko eines Datenverlusts', category: 'data_breach', likelihood: 4, impact: 5, inherent_risk: 'critical', mitigating_controls: ['PRIV-001', 'CRYPTO-001'], residual_likelihood: 2, residual_impact: 3, residual_risk: 'medium', owner: 'Security Team', status: 'open', treatment_plan: 'Encryption implementieren', }, { id: '2', risk_id: 'RISK-002', title: 'Compliance Gap', category: 'compliance_gap', likelihood: 3, impact: 3, inherent_risk: 'medium', status: 'mitigated', }, { id: '3', risk_id: 'RISK-003', title: 'Vendor Risk', category: 'vendor_risk', likelihood: 2, impact: 2, inherent_risk: 'low', status: 'accepted', }, ] const mockControls: Control[] = [ { id: 'c1', control_id: 'PRIV-001', title: 'Datenschutz-Massnahme', domain: 'priv', status: 'implemented', }, { id: 'c2', control_id: 'CRYPTO-001', title: 'Verschluesselung', domain: 'crypto', status: 'implemented', }, ] describe('calculateRiskLevel', () => { it('should return low for score 1-5', () => { expect(calculateRiskLevel(1, 1)).toBe('low') expect(calculateRiskLevel(1, 5)).toBe('low') expect(calculateRiskLevel(5, 1)).toBe('low') }) it('should return medium for score 6-11', () => { expect(calculateRiskLevel(2, 3)).toBe('medium') expect(calculateRiskLevel(3, 3)).toBe('medium') expect(calculateRiskLevel(2, 5)).toBe('medium') }) it('should return high for score 12-19', () => { expect(calculateRiskLevel(3, 4)).toBe('high') expect(calculateRiskLevel(4, 4)).toBe('high') expect(calculateRiskLevel(3, 5)).toBe('high') }) it('should return critical for score 20-25', () => { expect(calculateRiskLevel(4, 5)).toBe('critical') expect(calculateRiskLevel(5, 4)).toBe('critical') expect(calculateRiskLevel(5, 5)).toBe('critical') }) }) describe('RiskHeatmap', () => { it('should render the component with risks', () => { render() // Statistik-Header sollte sichtbar sein expect(screen.getByText('3')).toBeInTheDocument() // Gesamt expect(screen.getByText('Critical')).toBeInTheDocument() expect(screen.getByText('Medium')).toBeInTheDocument() expect(screen.getByText('Low')).toBeInTheDocument() }) it('should render empty state when no risks', () => { render() expect(screen.getByText(/Keine Risiken vorhanden/i)).toBeInTheDocument() }) it('should display risk badges in cells', () => { render() // Risk-IDs sollten als Badges angezeigt werden expect(screen.getByText('R001')).toBeInTheDocument() expect(screen.getByText('R002')).toBeInTheDocument() expect(screen.getByText('R003')).toBeInTheDocument() }) it('should support German language', () => { render() expect(screen.getByText('Gesamt')).toBeInTheDocument() expect(screen.getByText('Alle Kategorien')).toBeInTheDocument() expect(screen.getByText('Alle Status')).toBeInTheDocument() }) it('should support English language', () => { render() expect(screen.getByText('Total')).toBeInTheDocument() expect(screen.getByText('All Categories')).toBeInTheDocument() expect(screen.getByText('All Status')).toBeInTheDocument() }) it('should call onRiskClick when risk is clicked', () => { const onRiskClick = vi.fn() render() const riskBadge = screen.getByText('R001') fireEvent.click(riskBadge) expect(onRiskClick).toHaveBeenCalledWith(mockRisks[0]) }) it('should filter by category', () => { render() const categorySelect = screen.getByDisplayValue('Alle Kategorien') fireEvent.change(categorySelect, { target: { value: 'data_breach' } }) // Nur RISK-001 sollte sichtbar sein expect(screen.getByText('R001')).toBeInTheDocument() expect(screen.queryByText('R002')).not.toBeInTheDocument() }) it('should filter by status', () => { render() const statusSelect = screen.getByDisplayValue('Alle Status') fireEvent.change(statusSelect, { target: { value: 'mitigated' } }) // Nur RISK-002 sollte sichtbar sein expect(screen.queryByText('R001')).not.toBeInTheDocument() expect(screen.getByText('R002')).toBeInTheDocument() }) it('should show comparison view when enabled', () => { render() // View-Mode Buttons sollten sichtbar sein expect(screen.getByText('Vergleich')).toBeInTheDocument() expect(screen.getByText('Residual')).toBeInTheDocument() }) it('should display linked controls for risk', () => { render() // Klicke auf das erste Risiko const riskBadge = screen.getByText('R001') fireEvent.click(riskBadge) // Controls sollten in den Details erscheinen expect(screen.getByText('PRIV-001')).toBeInTheDocument() expect(screen.getByText('CRYPTO-001')).toBeInTheDocument() }) it('should show risk movement summary in comparison mode', () => { render() // Wechsle in Vergleichsmodus fireEvent.click(screen.getByText('Vergleich')) // Risikoveraenderung sollte angezeigt werden expect(screen.getByText(/Critical reduziert|Critical reduced/i)).toBeInTheDocument() }) }) describe('MiniRiskMatrix', () => { it('should render a compact matrix', () => { const { container } = render() // 5x5 Grid = 25 Zellen const cells = container.querySelectorAll('.w-6.h-6') expect(cells.length).toBe(25) }) it('should show risk counts in cells', () => { render() // RISK-001 bei L4/I5 = 1 // RISK-002 bei L3/I3 = 1 // RISK-003 bei L2/I2 = 1 const countElements = screen.getAllByText('1') expect(countElements.length).toBeGreaterThanOrEqual(3) }) it('should support different sizes', () => { const { container, rerender } = render() expect(container.querySelector('.w-6')).toBeInTheDocument() rerender() expect(container.querySelector('.w-8')).toBeInTheDocument() }) }) describe('RiskDistribution', () => { it('should render distribution bars', () => { render() expect(screen.getByText('critical')).toBeInTheDocument() expect(screen.getByText('high')).toBeInTheDocument() expect(screen.getByText('medium')).toBeInTheDocument() expect(screen.getByText('low')).toBeInTheDocument() }) it('should show correct counts', () => { render() // 1 critical, 0 high, 1 medium, 1 low const counts = screen.getAllByText('1') expect(counts.length).toBe(3) // critical, medium, low // 0 high expect(screen.getByText('0')).toBeInTheDocument() }) it('should support language prop', () => { render() // Englische Bezeichnungen werden verwendet expect(screen.getByText('critical')).toBeInTheDocument() }) })