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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user