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,72 @@
import { describe, it, expect, vi } from 'vitest'
import { render, fireEvent } from '@testing-library/react'
import { TipCard } from './TipCard'
describe('TipCard', () => {
const mockTip = {
id: 'tip1',
severity: 'info' as const,
messageKey: 'wrap_red_days',
data: {},
}
it('renders tip card with title', () => {
const { getByTestId } = render(<TipCard tip={mockTip} />)
expect(getByTestId('tip-card')).toBeInTheDocument()
expect(getByTestId('tip-card').textContent).toContain('Wrap Red Days')
})
it('applies severity class', () => {
const { getByTestId } = render(<TipCard tip={mockTip} />)
expect(getByTestId('tip-card')).toHaveClass('tip-card--info')
})
it('renders warning severity class', () => {
const warningTip = { ...mockTip, severity: 'warning' as const }
const { getByTestId } = render(<TipCard tip={warningTip} />)
expect(getByTestId('tip-card')).toHaveClass('tip-card--warning')
})
it('renders action severity class', () => {
const actionTip = { ...mockTip, severity: 'action' as const }
const { getByTestId } = render(<TipCard tip={actionTip} />)
expect(getByTestId('tip-card')).toHaveClass('tip-card--action')
})
it('renders dismiss button when onDismiss provided', () => {
const { getByTestId } = render(
<TipCard tip={mockTip} onDismiss={() => {}} />
)
expect(getByTestId('dismiss-button')).toBeInTheDocument()
})
it('does not render dismiss button when onDismiss not provided', () => {
const { queryByTestId } = render(<TipCard tip={mockTip} />)
expect(queryByTestId('dismiss-button')).not.toBeInTheDocument()
})
it('calls onDismiss with tip id when dismissed', () => {
const onDismiss = vi.fn()
const { getByTestId } = render(
<TipCard tip={mockTip} onDismiss={onDismiss} />
)
fireEvent.click(getByTestId('dismiss-button'))
expect(onDismiss).toHaveBeenCalledWith('tip1')
})
it('displays message key as body text', () => {
const { getByTestId } = render(<TipCard tip={mockTip} />)
expect(getByTestId('tip-card').textContent).toContain('wrap_red_days')
})
it('provides accessibility label', () => {
const { getByTestId } = render(<TipCard tip={mockTip} />)
expect(getByTestId('tip-card')).toHaveAttribute('aria-label', 'Tip: Wrap Red Days')
})
it('maps unknown messageKey to itself', () => {
const unknownTip = { ...mockTip, messageKey: 'unknown_key' }
const { getByTestId } = render(<TipCard tip={unknownTip} />)
expect(getByTestId('tip-card').textContent).toContain('unknown_key')
})
})