Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/testing/HEALTH_METRICS_TABS_TESTS.md
Original file line number Diff line number Diff line change
@@ -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.
141 changes: 141 additions & 0 deletions src/components/dashboard/CommitmentHealthMetrics.test.tsx
Original file line number Diff line number Diff line change
@@ -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
}) => (
<div data-testid="value-chart">
{JSON.stringify({ data, volatilityPercent })}
</div>
),
}))

vi.mock('./HealthMetricsDrawdownChart', () => ({
HealthMetricsDrawdownChart: ({
data,
thresholdPercent,
volatilityPercent,
}: {
data: Array<{ date: string; drawdownPercent: number }>
thresholdPercent?: number
volatilityPercent?: number
}) => (
<div data-testid="drawdown-chart">
{JSON.stringify({ data, thresholdPercent, volatilityPercent })}
</div>
),
}))

vi.mock('./HealthMetricsFeeGenerationChart', () => ({
HealthMetricsFeeGenerationChart: ({
data,
volatilityPercent,
}: {
data: Array<{ date: string; feeAmount: number }>
volatilityPercent?: number
}) => (
<div data-testid="fee-chart">
{JSON.stringify({ data, volatilityPercent })}
</div>
),
}))

vi.mock('./HealthMetricsComplianceChart', () => ({
HealthMetricsComplianceChart: ({
data,
}: {
data: Array<{ date: string; complianceScore: number }>
}) => (
<div data-testid="compliance-chart">
{JSON.stringify({ data })}
</div>
),
}))

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(<CommitmentHealthMetrics {...props} />)

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(<CommitmentHealthMetrics {...props} />)

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(<CommitmentHealthMetrics {...props} />)

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()
})
})
57 changes: 48 additions & 9 deletions src/components/dashboard/CommitmentHealthMetrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TabType, React.ReactNode> = {
value: <TrendingUp className="w-4 h-4" />,
drawdown: <TrendingDown className="w-4 h-4" />,
Expand All @@ -41,23 +48,50 @@ export default function CommitmentHealthMetrics({
}: CommitmentHealthMetricsProps) {
const [activeTab, setActiveTab] = useState<TabType>('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<HTMLButtonElement>, 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<HTMLButtonElement>('[role="tab"]')
[nextIndex]?.focus();
}
};

return (
<div className="w-full bg-[#0a0a0a] rounded-2xl p-6 border border-[#222]">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8">
<h2 className="text-2xl font-semibold text-white">Health Metrics</h2>

<div className="flex flex-wrap gap-2 p-1 bg-[#111] rounded-lg border border-[#222]">
{tabs.map((tab) => (
<div
className="flex flex-wrap gap-2 p-1 bg-[#111] rounded-lg border border-[#222]"
role="tablist"
aria-label="Health metric charts"
>
{tabs.map((tab, index) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
onKeyDown={(event) => handleTabKeyDown(event, index)}
role="tab"
id={`health-metric-tab-${tab.id}`}
aria-selected={activeTab === tab.id}
aria-controls={`health-metric-panel-${tab.id}`}
tabIndex={activeTab === tab.id ? 0 : -1}
className={cn(
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200',
activeTab === tab.id
Expand All @@ -72,7 +106,12 @@ export default function CommitmentHealthMetrics({
</div>
</div>

<div className="w-full">
<div
className="w-full"
role="tabpanel"
id={`health-metric-panel-${activeTab}`}
aria-labelledby={`health-metric-tab-${activeTab}`}
>
{activeTab === 'value' && (
<HealthMetricsValueHistoryChart
data={valueHistoryData}
Expand Down