From e9cf7e5cc132781c2af77c95ede6f9229e4ff66a Mon Sep 17 00:00:00 2001 From: Will Matos Date: Tue, 23 Jun 2026 14:19:39 -0500 Subject: [PATCH] Add health metrics tab coverage --- docs/testing/HEALTH_METRICS_TABS_TESTS.md | 13 ++ .../CommitmentHealthMetrics.test.tsx | 141 ++++++++++++++++++ .../dashboard/CommitmentHealthMetrics.tsx | 57 +++++-- 3 files changed, 202 insertions(+), 9 deletions(-) create mode 100644 docs/testing/HEALTH_METRICS_TABS_TESTS.md create mode 100644 src/components/dashboard/CommitmentHealthMetrics.test.tsx diff --git a/docs/testing/HEALTH_METRICS_TABS_TESTS.md b/docs/testing/HEALTH_METRICS_TABS_TESTS.md new file mode 100644 index 00000000..faab4427 --- /dev/null +++ b/docs/testing/HEALTH_METRICS_TABS_TESTS.md @@ -0,0 +1,13 @@ +# Health Metrics Tabs Tests + +`src/components/dashboard/CommitmentHealthMetrics.test.tsx` covers the tab behavior for the health metrics card. + +## Coverage + +- Default Value History tab selection. +- `role="tablist"`, `role="tab"`, `aria-selected`, and tabpanel labelling. +- Click switching across Value History, Drawdown, Fee Generation, and Compliance. +- Data handoff from each tab to its corresponding chart component. +- Arrow key, Home, and End keyboard navigation. + +The chart components are mocked in the test so the suite can focus on tab state, accessibility, and prop routing without rendering Recharts internals. diff --git a/src/components/dashboard/CommitmentHealthMetrics.test.tsx b/src/components/dashboard/CommitmentHealthMetrics.test.tsx new file mode 100644 index 00000000..ac227f15 --- /dev/null +++ b/src/components/dashboard/CommitmentHealthMetrics.test.tsx @@ -0,0 +1,141 @@ +// @vitest-environment happy-dom + +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import CommitmentHealthMetrics from './CommitmentHealthMetrics' + +vi.mock('./HealthMetricsValueHistoryChart', () => ({ + HealthMetricsValueHistoryChart: ({ + data, + volatilityPercent, + }: { + data: Array<{ date: string; currentValue: number; initialAmount?: number }> + volatilityPercent?: number + }) => ( +
+ {JSON.stringify({ data, volatilityPercent })} +
+ ), +})) + +vi.mock('./HealthMetricsDrawdownChart', () => ({ + HealthMetricsDrawdownChart: ({ + data, + thresholdPercent, + volatilityPercent, + }: { + data: Array<{ date: string; drawdownPercent: number }> + thresholdPercent?: number + volatilityPercent?: number + }) => ( +
+ {JSON.stringify({ data, thresholdPercent, volatilityPercent })} +
+ ), +})) + +vi.mock('./HealthMetricsFeeGenerationChart', () => ({ + HealthMetricsFeeGenerationChart: ({ + data, + volatilityPercent, + }: { + data: Array<{ date: string; feeAmount: number }> + volatilityPercent?: number + }) => ( +
+ {JSON.stringify({ data, volatilityPercent })} +
+ ), +})) + +vi.mock('./HealthMetricsComplianceChart', () => ({ + HealthMetricsComplianceChart: ({ + data, + }: { + data: Array<{ date: string; complianceScore: number }> + }) => ( +
+ {JSON.stringify({ data })} +
+ ), +})) + +const props = { + complianceData: [{ date: 'Jun 3', complianceScore: 98 }], + drawdownData: [{ date: 'Jun 2', drawdownPercent: 0.12 }], + valueHistoryData: [{ date: 'Jun 1', currentValue: 1500, initialAmount: 1000 }], + feeGenerationData: [{ date: 'Jun 4', feeAmount: 42 }], + thresholdPercent: 0.2, + volatilityPercent: 17, +} + +function getTabs() { + return { + value: screen.getByRole('tab', { name: /value history/i }), + drawdown: screen.getByRole('tab', { name: /drawdown/i }), + fee: screen.getByRole('tab', { name: /fee generation/i }), + compliance: screen.getByRole('tab', { name: /compliance/i }), + } +} + +describe('CommitmentHealthMetrics', () => { + it('renders the value tab by default with tab semantics and value data', () => { + render() + + const tabs = getTabs() + + expect(screen.getByRole('tablist', { name: 'Health metric charts' })).toBeInTheDocument() + expect(tabs.value).toHaveAttribute('aria-selected', 'true') + expect(tabs.drawdown).toHaveAttribute('aria-selected', 'false') + expect(screen.getByRole('tabpanel')).toHaveAttribute('aria-labelledby', 'health-metric-tab-value') + expect(screen.getByTestId('value-chart')).toHaveTextContent('"currentValue":1500') + expect(screen.getByTestId('value-chart')).toHaveTextContent('"volatilityPercent":17') + }) + + it('switches to each chart tab and passes the matching data props', () => { + render() + + fireEvent.click(getTabs().drawdown) + expect(getTabs().drawdown).toHaveAttribute('aria-selected', 'true') + expect(screen.getByRole('tabpanel')).toHaveAttribute('aria-labelledby', 'health-metric-tab-drawdown') + expect(screen.getByTestId('drawdown-chart')).toHaveTextContent('"drawdownPercent":0.12') + expect(screen.getByTestId('drawdown-chart')).toHaveTextContent('"thresholdPercent":0.2') + + fireEvent.click(getTabs().fee) + expect(getTabs().fee).toHaveAttribute('aria-selected', 'true') + expect(screen.getByRole('tabpanel')).toHaveAttribute('aria-labelledby', 'health-metric-tab-fee') + expect(screen.getByTestId('fee-chart')).toHaveTextContent('"feeAmount":42') + + fireEvent.click(getTabs().compliance) + expect(getTabs().compliance).toHaveAttribute('aria-selected', 'true') + expect(screen.getByRole('tabpanel')).toHaveAttribute('aria-labelledby', 'health-metric-tab-compliance') + expect(screen.getByTestId('compliance-chart')).toHaveTextContent('"complianceScore":98') + }) + + it('supports arrow, Home, and End keyboard navigation across tabs', async () => { + render() + + const tabs = getTabs() + + tabs.value.focus() + fireEvent.keyDown(tabs.value, { key: 'ArrowRight' }) + + await waitFor(() => expect(getTabs().drawdown).toHaveAttribute('aria-selected', 'true')) + expect(getTabs().drawdown).toHaveFocus() + + fireEvent.keyDown(getTabs().drawdown, { key: 'ArrowLeft' }) + + await waitFor(() => expect(getTabs().value).toHaveAttribute('aria-selected', 'true')) + expect(getTabs().value).toHaveFocus() + + fireEvent.keyDown(getTabs().value, { key: 'End' }) + + await waitFor(() => expect(getTabs().compliance).toHaveAttribute('aria-selected', 'true')) + expect(getTabs().compliance).toHaveFocus() + + fireEvent.keyDown(getTabs().compliance, { key: 'Home' }) + + await waitFor(() => expect(getTabs().value).toHaveAttribute('aria-selected', 'true')) + expect(getTabs().value).toHaveFocus() + }) +}) diff --git a/src/components/dashboard/CommitmentHealthMetrics.tsx b/src/components/dashboard/CommitmentHealthMetrics.tsx index 416ef633..6148155a 100644 --- a/src/components/dashboard/CommitmentHealthMetrics.tsx +++ b/src/components/dashboard/CommitmentHealthMetrics.tsx @@ -15,6 +15,13 @@ function cn(...inputs: ClassValue[]) { type TabType = 'value' | 'drawdown' | 'fee' | 'compliance'; +const tabs: { id: TabType; label: string }[] = [ + { id: 'value', label: 'Value History' }, + { id: 'drawdown', label: 'Drawdown' }, + { id: 'fee', label: 'Fee Generation' }, + { id: 'compliance', label: 'Compliance' }, +]; + const tabIcons: Record = { value: , drawdown: , @@ -41,23 +48,50 @@ export default function CommitmentHealthMetrics({ }: CommitmentHealthMetricsProps) { const [activeTab, setActiveTab] = useState('value'); - const tabs: { id: TabType; label: string }[] = [ - { id: 'value', label: 'Value History' }, - { id: 'drawdown', label: 'Drawdown' }, - { id: 'fee', label: 'Fee Generation' }, - { id: 'compliance', label: 'Compliance' }, - ]; + const handleTabKeyDown = (event: React.KeyboardEvent, index: number) => { + const lastIndex = tabs.length - 1; + let nextIndex: number | null = null; + + if (event.key === 'ArrowRight') { + nextIndex = index === lastIndex ? 0 : index + 1; + } else if (event.key === 'ArrowLeft') { + nextIndex = index === 0 ? lastIndex : index - 1; + } else if (event.key === 'Home') { + nextIndex = 0; + } else if (event.key === 'End') { + nextIndex = lastIndex; + } + + if (nextIndex !== null) { + event.preventDefault(); + setActiveTab(tabs[nextIndex].id); + event.currentTarget + .closest('[role="tablist"]') + ?.querySelectorAll('[role="tab"]') + [nextIndex]?.focus(); + } + }; return (

Health Metrics

-
- {tabs.map((tab) => ( +
+ {tabs.map((tab, index) => (
-
+
{activeTab === 'value' && (