Files
F-r-ldraledigplannerare/frontend/src/components/inputs/TipCard.test.tsx
T
jacobh2 c15cd44cb4 Warn when saving unevenly costs the family days
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.
2026-09-17 22:31:37 +02:00

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)
},
)
})