diff --git a/src/components/dashboard/__tests__/HealthMetricsCharts2.test.tsx b/src/components/dashboard/__tests__/HealthMetricsCharts2.test.tsx new file mode 100644 index 00000000..fce64cd2 --- /dev/null +++ b/src/components/dashboard/__tests__/HealthMetricsCharts2.test.tsx @@ -0,0 +1,280 @@ +/** + * @vitest-environment happy-dom + * + * RTL tests for HealthMetricsComplianceChart and HealthMetricsFeeGenerationChart. + * Covers: representative series, empty series, single-point series, 100% compliance + * ceiling, zero fees, and tooltip rendering. + */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi, beforeAll } from 'vitest'; +import '@testing-library/jest-dom/vitest'; + +// Mock ResizeObserver so Recharts' ResponsiveContainer works under happy-dom +beforeAll(() => { + global.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} + }; +}); + +// Mock recharts ResponsiveContainer to provide real dimensions +vi.mock('recharts', async () => { + const actual = await vi.importActual('recharts'); + return { + ...actual, + ResponsiveContainer: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + }; +}); + +// Keep VolatilityExposureMeter isolated — it has its own tests +vi.mock('@/components/VolatilityExposureMeter/VolatilityExposureMeter', () => ({ + default: () =>
, +})); + +import { HealthMetricsComplianceChart } from '../HealthMetricsComplianceChart'; +import { HealthMetricsFeeGenerationChart } from '../HealthMetricsFeeGenerationChart'; + +// --------------------------------------------------------------------------- +// Shared fixtures +// --------------------------------------------------------------------------- +const representativeCompliance = [ + { date: 'Jan', complianceScore: 72 }, + { date: 'Feb', complianceScore: 85 }, + { date: 'Mar', complianceScore: 91 }, +]; + +const representativeFees = [ + { date: 'Jan', feeAmount: 250 }, + { date: 'Feb', feeAmount: 875 }, + { date: 'Mar', feeAmount: 1340 }, +]; + +// --------------------------------------------------------------------------- +// HealthMetricsComplianceChart +// --------------------------------------------------------------------------- +describe('HealthMetricsComplianceChart — representative series', () => { + it('renders without crashing', () => { + const { container } = render( + , + ); + expect(container.firstChild).toBeTruthy(); + }); + + it('renders the description footer text', () => { + render(); + expect( + screen.getByText(/compliance score/i), + ).toBeInTheDocument(); + }); +}); + +describe('HealthMetricsComplianceChart — empty series', () => { + it('renders without crashing when data is empty', () => { + const { container } = render(); + expect(container.firstChild).toBeTruthy(); + }); + + it('still renders the description footer on empty data', () => { + render(); + expect(screen.getByText(/compliance score/i)).toBeInTheDocument(); + }); +}); + +describe('HealthMetricsComplianceChart — single data point', () => { + it('renders a single-point series', () => { + const { container } = render( + , + ); + expect(container.firstChild).toBeTruthy(); + }); +}); + +describe('HealthMetricsComplianceChart — 100% compliance ceiling', () => { + it('renders scores at the maximum value of 100', () => { + const { container } = render( + , + ); + expect(container.firstChild).toBeTruthy(); + }); +}); + +// --------------------------------------------------------------------------- +// HealthMetricsComplianceChart — CustomTooltip (inline replica) +// --------------------------------------------------------------------------- +describe('HealthMetricsComplianceChart — CustomTooltip behaviour', () => { + // Mirror the tooltip contract; the private component is not exported so + // we replicate its render logic to keep the assertion readable. + const ComplianceTooltip = ({ + active, + payload, + label, + }: { + active?: boolean; + payload?: Array<{ value: number }>; + label?: string; + }) => { + if (active && payload && payload.length) { + return ( +
+

{label}

+

Score: {payload[0].value}

+
+ ); + } + return null; + }; + + it('returns null when not active', () => { + const { container } = render( + , + ); + expect(container.firstChild).toBeNull(); + }); + + it('returns null when payload is empty', () => { + const { container } = render( + , + ); + expect(container.firstChild).toBeNull(); + }); + + it('renders label and score when active with payload', () => { + render( + , + ); + expect(screen.getByText('Mar')).toBeInTheDocument(); + expect(screen.getByText('Score: 91')).toBeInTheDocument(); + }); +}); + +// --------------------------------------------------------------------------- +// HealthMetricsFeeGenerationChart +// --------------------------------------------------------------------------- +describe('HealthMetricsFeeGenerationChart — representative series', () => { + it('renders without crashing', () => { + const { container } = render( + , + ); + expect(container.firstChild).toBeTruthy(); + }); + + it('renders the description footer text', () => { + render(); + expect(screen.getByText(/fees generated/i)).toBeInTheDocument(); + }); +}); + +describe('HealthMetricsFeeGenerationChart — empty series', () => { + it('renders without crashing when data is empty', () => { + const { container } = render(); + expect(container.firstChild).toBeTruthy(); + }); +}); + +describe('HealthMetricsFeeGenerationChart — single data point', () => { + it('renders a single-point series', () => { + const { container } = render( + , + ); + expect(container.firstChild).toBeTruthy(); + }); +}); + +describe('HealthMetricsFeeGenerationChart — zero fees', () => { + it('renders when all fee amounts are zero', () => { + const { container } = render( + , + ); + expect(container.firstChild).toBeTruthy(); + }); +}); + +describe('HealthMetricsFeeGenerationChart — volatility meter integration', () => { + it('renders VolatilityExposureMeter when volatilityPercent is provided', () => { + render( + , + ); + expect(screen.getByTestId('volatility-meter')).toBeInTheDocument(); + }); + + it('does not render VolatilityExposureMeter when volatilityPercent is omitted', () => { + render(); + expect(screen.queryByTestId('volatility-meter')).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// HealthMetricsFeeGenerationChart — CustomTooltip (inline replica) +// --------------------------------------------------------------------------- +describe('HealthMetricsFeeGenerationChart — CustomTooltip behaviour', () => { + const FeeTooltip = ({ + active, + payload, + label, + }: { + active?: boolean; + payload?: Array<{ value: number }>; + label?: string; + }) => { + if (active && payload && payload.length) { + return ( +
+

{label}

+

Fees: ${(payload[0].value ?? 0).toLocaleString()}

+
+ ); + } + return null; + }; + + it('returns null when not active', () => { + const { container } = render( + , + ); + expect(container.firstChild).toBeNull(); + }); + + it('returns null when payload is empty', () => { + const { container } = render( + , + ); + expect(container.firstChild).toBeNull(); + }); + + it('renders label and formatted fee when active', () => { + render( + , + ); + expect(screen.getByText('Mar')).toBeInTheDocument(); + expect(screen.getByText(/Fees:.*1,340/)).toBeInTheDocument(); + }); + + it('renders zero fee correctly', () => { + render( + , + ); + expect(screen.getByText(/Fees:.*0/)).toBeInTheDocument(); + }); +});