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() expect(getByTestId('tip-card')).toBeInTheDocument() expect(getByTestId('tip-card').textContent).toContain(sv['tip.wrapRedDaysWithVacation.title']) }) it('applies severity class', () => { const { getByTestId } = render() expect(getByTestId('tip-card')).toHaveClass('tip-card--info') }) it('renders warning severity class', () => { const warningTip = { ...mockTip, severity: 'warning' as const } const { getByTestId } = render() expect(getByTestId('tip-card')).toHaveClass('tip-card--warning') }) it('renders action severity class', () => { const actionTip = { ...mockTip, severity: 'action' as const } const { getByTestId } = render() expect(getByTestId('tip-card')).toHaveClass('tip-card--action') }) it('renders dismiss button when onDismiss provided', () => { const { getByTestId } = render( {}} /> ) expect(getByTestId('dismiss-button')).toBeInTheDocument() }) it('does not render dismiss button when onDismiss not provided', () => { const { queryByTestId } = render() expect(queryByTestId('dismiss-button')).not.toBeInTheDocument() }) it('calls onDismiss with tip id when dismissed', () => { const onDismiss = vi.fn() const { getByTestId } = render( ) fireEvent.click(getByTestId('dismiss-button')) expect(onDismiss).toHaveBeenCalledWith('tip1') }) it('interpolates the tip data into the translated body text', () => { const { getByTestId } = render() expect(getByTestId('tip-card').textContent).toContain('Alex') expect(getByTestId('tip-card').textContent).toContain('2026-12-24') }) it('provides accessibility label', () => { const { getByTestId } = render() 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() 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() 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) }, ) })