Phase 2: Build 9 presentational components with full test coverage

New components (all with .tsx + .test.tsx, 90%+ coverage):
- CalendarDay: day cell with event badges
- CalendarEvent: colored event chip with dismiss option
- DaysBreakdown: table of days by type
- ProgressChart: progress bar toward age-4 deadline
- TipCard: optimization tip card with severity badges
- VacationDaysInput: number input with min/max validation
- ParentProfileInput: dual selects for salary type & employment status
- PlanningGoalSelector: radio buttons for planning goal
- BreakPeriodsInput: list manager with inline add/edit form

All components follow existing patterns from Header/Footer:
- Typed props interfaces (exported)
- Semantic HTML with data-testid & ARIA attributes
- No business logic, purely presentational
- Comprehensive test coverage including edge cases

npm test: 22 files, 167 tests passing
npx tsc --noEmit: clean

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 17:50:23 +02:00
parent faebcced8a
commit 425e29ea21
19 changed files with 2048 additions and 0 deletions
@@ -0,0 +1,50 @@
import { describe, it, expect } from 'vitest'
import { render } from '@testing-library/react'
import { DaysBreakdown } from './DaysBreakdown'
describe('DaysBreakdown', () => {
it('renders empty state when no breakdown provided', () => {
const { getByTestId } = render(<DaysBreakdown breakdown={[]} />)
expect(getByTestId('days-breakdown').textContent).toContain('No days recorded yet')
})
it('renders table with breakdown data', () => {
const breakdown = [
{ type: 'vacation', count: 10 },
{ type: 'sick leave', count: 3 },
{ type: 'parental', count: 50 },
]
const { getByTestId } = render(<DaysBreakdown breakdown={breakdown} />)
const table = getByTestId('days-breakdown')
expect(table.querySelector('table')).toBeInTheDocument()
expect(table.textContent).toContain('vacation')
expect(table.textContent).toContain('10')
})
it('renders correct number of rows', () => {
const breakdown = [
{ type: 'vacation', count: 10 },
{ type: 'sick leave', count: 3 },
{ type: 'parental', count: 50 },
]
const { getByTestId } = render(<DaysBreakdown breakdown={breakdown} />)
expect(getByTestId('breakdown-row-vacation')).toBeInTheDocument()
expect(getByTestId('breakdown-row-sick leave')).toBeInTheDocument()
expect(getByTestId('breakdown-row-parental')).toBeInTheDocument()
})
it('displays correct values in table', () => {
const breakdown = [{ type: 'vacation', count: 25 }]
const { getByTestId } = render(<DaysBreakdown breakdown={breakdown} />)
const row = getByTestId('breakdown-row-vacation')
expect(row.textContent).toContain('vacation')
expect(row.textContent).toContain('25')
})
it('renders table headers', () => {
const breakdown = [{ type: 'vacation', count: 10 }]
const { getByTestId } = render(<DaysBreakdown breakdown={breakdown} />)
const table = getByTestId('days-breakdown').querySelector('table')
expect(table?.querySelector('thead')).toBeInTheDocument()
})
})
@@ -0,0 +1,36 @@
import { ReactNode } from 'react'
export interface DaysBreakdownProps {
breakdown: Array<{ type: string; count: number }>
}
export function DaysBreakdown({ breakdown }: DaysBreakdownProps): ReactNode {
if (!breakdown || breakdown.length === 0) {
return (
<div data-testid="days-breakdown" className="days-breakdown">
<p className="days-breakdown__empty">No days recorded yet</p>
</div>
)
}
return (
<div data-testid="days-breakdown" className="days-breakdown">
<table className="days-breakdown__table">
<thead>
<tr>
<th>Leave Type</th>
<th>Days</th>
</tr>
</thead>
<tbody>
{breakdown.map((item, idx) => (
<tr key={idx} data-testid={`breakdown-row-${item.type}`}>
<td>{item.type}</td>
<td>{item.count}</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
@@ -0,0 +1,63 @@
import { describe, it, expect } from 'vitest'
import { render } from '@testing-library/react'
import { ProgressChart } from './ProgressChart'
describe('ProgressChart', () => {
it('renders progress chart', () => {
const { getByTestId } = render(
<ProgressChart currentWeeks={10} targetWeeks={50} />
)
expect(getByTestId('progress-chart')).toBeInTheDocument()
})
it('calculates percentage correctly', () => {
const { getByTestId } = render(
<ProgressChart currentWeeks={25} targetWeeks={100} />
)
expect(getByTestId('percentage').textContent).toBe('25%')
})
it('displays weeks correctly', () => {
const { getByTestId } = render(
<ProgressChart currentWeeks={10} targetWeeks={50} />
)
expect(getByTestId('current-weeks').textContent).toBe('10 / 50 weeks')
})
it('sets fill width based on percentage', () => {
const { getByTestId } = render(
<ProgressChart currentWeeks={50} targetWeeks={100} />
)
const fill = getByTestId('progress-fill')
expect(fill).toHaveStyle({ width: '50%' })
})
it('caps percentage at 100%', () => {
const { getByTestId } = render(
<ProgressChart currentWeeks={150} targetWeeks={100} />
)
expect(getByTestId('percentage').textContent).toBe('100%')
expect(getByTestId('progress-fill')).toHaveStyle({ width: '100%' })
})
it('shows 0% when no progress', () => {
const { getByTestId } = render(
<ProgressChart currentWeeks={0} targetWeeks={50} />
)
expect(getByTestId('percentage').textContent).toBe('0%')
})
it('displays child age when provided', () => {
const { getByTestId } = render(
<ProgressChart currentWeeks={10} targetWeeks={50} childAge={2} />
)
expect(getByTestId('progress-chart').textContent).toContain('Age: 2')
})
it('handles zero target weeks', () => {
const { getByTestId } = render(
<ProgressChart currentWeeks={10} targetWeeks={0} />
)
expect(getByTestId('percentage').textContent).toBe('0%')
})
})
@@ -0,0 +1,39 @@
import { ReactNode } from 'react'
export interface ProgressChartProps {
currentWeeks: number
targetWeeks: number
childAge?: number
}
export function ProgressChart({
currentWeeks,
targetWeeks,
childAge,
}: ProgressChartProps): ReactNode {
const percentage = targetWeeks > 0 ? Math.min((currentWeeks / targetWeeks) * 100, 100) : 0
return (
<div data-testid="progress-chart" className="progress-chart">
<div className="progress-chart__header">
<h3>Parental Leave Progress</h3>
{childAge !== undefined && <span className="progress-chart__age">Age: {childAge}</span>}
</div>
<div className="progress-chart__bar">
<div
className="progress-chart__fill"
style={{ width: `${percentage}%` }}
data-testid="progress-fill"
/>
</div>
<div className="progress-chart__stats">
<span data-testid="current-weeks">
{currentWeeks} / {targetWeeks} weeks
</span>
<span data-testid="percentage" className="progress-chart__percentage">
{Math.round(percentage)}%
</span>
</div>
</div>
)
}