c15cd44cb4
Only ninety-six days survive a child's fourth birthday, and with two parents that is forty-eight each rather than a shared pool. A parent sitting on sixty days while the other has ten therefore forfeits twelve that the family could have kept, and nothing said so. The advice leads with taking the days as leave before the birthday, which turns them into actual time at home, and offers transferring only as the fallback that merely preserves them. The transferable figure is bounded by what Försäkringskassan actually reports as transferable, since the reserved days cannot move, and the copy says plainly that a transfer happens there and not in this app. Without the partner's figures the tip hedges rather than inventing a number.
119 lines
4.7 KiB
TypeScript
119 lines
4.7 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest'
|
|
import { render, fireEvent } from '@testing-library/react'
|
|
import { TipCard } from './TipCard'
|
|
import { sv } from '@/i18n/sv'
|
|
|
|
describe('TipCard', () => {
|
|
const mockTip = {
|
|
id: 'tip1',
|
|
severity: 'info' as const,
|
|
messageKey: 'tips.wrap_red_days_with_vacation',
|
|
data: { parent_name: 'Alex', cluster_start: '2026-12-24', cluster_end: '2026-12-26' },
|
|
}
|
|
|
|
it('renders tip card with title', () => {
|
|
const { getByTestId } = render(<TipCard tip={mockTip} />)
|
|
expect(getByTestId('tip-card')).toBeInTheDocument()
|
|
expect(getByTestId('tip-card').textContent).toContain(sv['tip.wrapRedDaysWithVacation.title'])
|
|
})
|
|
|
|
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('interpolates the tip data into the translated body text', () => {
|
|
const { getByTestId } = render(<TipCard tip={mockTip} />)
|
|
expect(getByTestId('tip-card').textContent).toContain('Alex')
|
|
expect(getByTestId('tip-card').textContent).toContain('2026-12-24')
|
|
})
|
|
|
|
it('provides accessibility label', () => {
|
|
const { getByTestId } = render(<TipCard tip={mockTip} />)
|
|
expect(getByTestId('tip-card')).toHaveAttribute(
|
|
'aria-label',
|
|
`Tips: ${sv['tip.wrapRedDaysWithVacation.title']}`,
|
|
)
|
|
})
|
|
|
|
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')
|
|
})
|
|
|
|
// Mirrors every `message_key="..."` literal emitted by
|
|
// backend/app/services/tips_service.py — verified with:
|
|
// git grep -oh 'message_key="[^"]*"' backend/app/services/tips_service.py | sort -u
|
|
// This is the test that would have caught the original bug: two of these
|
|
// eleven keys had no entry in TIP_TRANSLATION_KEYS, so their cards rendered
|
|
// with an empty title and body. If a twelfth backend rule is added and this
|
|
// list isn't updated to match, that new key isn't exercised here — but any
|
|
// key in this list that TipCard doesn't recognize now fails loudly.
|
|
const ALL_BACKEND_MESSAGE_KEYS = [
|
|
'tips.use_vacation_before_expiry',
|
|
'tips.balance_between_parents',
|
|
'tips.preserve_for_age_4',
|
|
'tips.age4_pooling_loss_exact',
|
|
'tips.age4_pooling_loss_hedged',
|
|
'tips.use_dubbeldagar_strategically',
|
|
'tips.lagsta_niva_coverage',
|
|
'tips.stretch_time_at_home',
|
|
'tips.sgi_minimum_after_age_1',
|
|
'tips.wrap_weekend_with_vacation',
|
|
'tips.wrap_red_days_with_vacation',
|
|
'tips.long_break_parent_swap',
|
|
'tips.reserve_for_preschool_planning_days',
|
|
]
|
|
|
|
it.each(ALL_BACKEND_MESSAGE_KEYS)(
|
|
'renders a non-empty, translated title and body for %s',
|
|
(messageKey) => {
|
|
const tip = { id: 'tip-x', severity: 'info' as const, messageKey, data: {} }
|
|
const { getByTestId } = render(<TipCard tip={tip} />)
|
|
const title = getByTestId('tip-card').querySelector('.tip-card__title')
|
|
const body = getByTestId('tip-card').querySelector('.tip-card__body')
|
|
|
|
expect(title?.textContent).toBeTruthy()
|
|
expect(body?.textContent).toBeTruthy()
|
|
// A falsy translation lookup falls back to rendering the raw key —
|
|
// that's the "blank card" bug in disguise, so guard against it too.
|
|
expect(title?.textContent).not.toBe(messageKey)
|
|
expect(body?.textContent).not.toBe(messageKey)
|
|
},
|
|
)
|
|
})
|