import { describe, it, expect, beforeEach, vi } from 'vitest' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { BrowserRouter } from 'react-router-dom' import { Dashboard } from './Dashboard' import * as hooks from '@/hooks' import { useWorkspace, useAuth } from '@/context' vi.mock('@/hooks') vi.mock('@/context') vi.mock('@/services/api') // Layout pulls in useAuth/useNavigate, which need a full AuthProvider/Router tree // to work. Dashboard.tsx only cares that its content renders inside Layout, so // stub Layout out — same pattern used by Calendar.test.tsx / Settings.test.tsx. vi.mock('@/components/layout/Layout', () => ({ Layout: ({ children }: { children: React.ReactNode }) =>
{children}
, })) const renderWithRouter = (component: React.ReactElement) => { return render({component}) } describe('Dashboard', () => { const mockWorkspaceContext = { workspace: { id: 'ws-1', name: 'Test Workspace', planningGoal: 'balanced' as const, createdAt: '2026-01-01' }, isLoading: false, error: null, workspaceId: 'ws-1', setWorkspaceId: vi.fn(), refetch: vi.fn(), children: [{ id: 'c-1', name: 'Child 1', birth_date: '2022-01-01' }], selectedChildId: 'c-1', selectChild: vi.fn(), personalDataUuid: 'pd-1', parentName: 'Test Parent', refreshContext: vi.fn(), } const mockAnalyticsData = { workspaceId: 'ws-1', childId: 'c-1', parentProjections: [], combinedDaysUsed: 30, combinedDaysLeft: 270, combinedExpiringAtB4: 100, } const mockParentalLeaveData = { id: 'pf-1', personalDataUuid: 'pd-1', workspaceId: 'ws-1', childId: 'c-1', source: 'scraped' as const, fetchedAt: '2026-06-13T10:00:00Z', totalDays: 300, daysUsed: 30, daysLeft: 270, reservedDaysTotal: 90, reservedDaysLeft: 80, transferableDaysLeft: 50, dubbeldagarLeft: 60, tierData: { sjukpenningTotal: 100, sjukpenningUsed: 10, sjukpenningLeft: 90, lagstaNivaTotal: 200, lagstaNivaUsed: 20, lagstaNivaLeft: 180, }, createdAt: '2026-06-13T10:00:00Z', updatedAt: '2026-06-13T10:00:00Z', } beforeEach(() => { vi.clearAllMocks() vi.mocked(useAuth).mockReturnValue({ accessToken: 'token', isAuthenticated: true, isLoading: false, login: vi.fn(), register: vi.fn(), logout: vi.fn(), } as any) vi.mocked(useWorkspace).mockReturnValue(mockWorkspaceContext as any) vi.mocked(hooks.useAnalytics).mockReturnValue({ data: mockAnalyticsData, isLoading: false, error: null, refresh: vi.fn(), } as any) vi.mocked(hooks.useParentalLeave).mockReturnValue({ data: mockParentalLeaveData, isLoading: false, error: null, refresh: vi.fn(), } as any) vi.mocked(hooks.usePlannedDays).mockReturnValue({ data: [ { id: '1', workspaceId: 'ws-1', childId: 'c-1', parentPersonalDataUuid: 'pd-1', date: '2026-06-15', dayType: 'vacation', isDubbeldag: false, createdAt: '', updatedAt: '' }, { id: '2', workspaceId: 'ws-1', childId: 'c-1', parentPersonalDataUuid: 'pd-1', date: '2026-06-16', dayType: 'sjukpenning', isDubbeldag: false, createdAt: '', updatedAt: '' }, ], isLoading: false, error: null, add: vi.fn(), remove: vi.fn(), } as any) vi.mocked(hooks.useScraperFetch).mockReturnValue({ sessionId: null, pollData: null, resolvedIds: null, error: null, isUnauthorized: false, start: vi.fn(), cancel: vi.fn(), isRunning: false, isTerminal: false, } as any) vi.mocked(hooks.useWorkspaceMembers).mockReturnValue({ data: [ { uuid: 'pd-1', workspaceId: 'ws-1', name: 'Alex', salaryType: 'monthly', employmentStatus: 'employed', createdAt: '2026-01-01' }, { uuid: 'pd-2', workspaceId: 'ws-1', name: 'Sam', salaryType: 'monthly', employmentStatus: 'employed', createdAt: '2026-01-01' }, ], isLoading: false, error: null, refresh: vi.fn(), } as any) }) it('renders loading spinner initially', () => { vi.mocked(useWorkspace).mockReturnValue({ ...mockWorkspaceContext, isLoading: true, } as any) renderWithRouter() expect(screen.getByText(/Laddar/)).toBeInTheDocument() }) it('shows add child form when no children', () => { vi.mocked(useWorkspace).mockReturnValue({ ...mockWorkspaceContext, children: [], selectedChildId: null, } as any) renderWithRouter() expect(screen.getByText(/Du har inga barn registrerade ännu/)).toBeInTheDocument() expect(screen.getByLabelText(/Barnets namn/)).toBeInTheDocument() }) it('shows overview metrics when children exist', () => { renderWithRouter() expect(screen.getByTestId('overview-metrics')).toBeInTheDocument() expect(screen.getByText(/Använda dagar/)).toBeInTheDocument() }) it('shows parental leave breakdown when data exists', () => { renderWithRouter() expect(screen.getByTestId('parental-leave-breakdown')).toBeInTheDocument() // ParentalLeaveBreakdown shows an explanatory note about reserved days // rather than a "Reserverade dagar" heading/row — see // ParentalLeaveBreakdown.test.tsx ("displays a reserved-days explanatory // note instead of a countdown"). expect(screen.getByTestId('reserved-days-note')).toHaveTextContent(/reserverade/i) }) it('renders FK fetch button', () => { renderWithRouter() expect(screen.getByRole('button', { name: /Hämta från Försäkringskassan/ })).toBeInTheDocument() }) it('disables FK fetch button when no child selected', () => { vi.mocked(useWorkspace).mockReturnValue({ ...mockWorkspaceContext, selectedChildId: null, } as any) renderWithRouter() const button = screen.getByRole('button', { name: /Hämta från Försäkringskassan/ }) expect(button).toBeDisabled() }) it('enables FK fetch button when child selected', () => { renderWithRouter() const button = screen.getByRole('button', { name: /Hämta från Försäkringskassan/ }) expect(button).not.toBeDisabled() }) it('shows planned days breakdown', () => { renderWithRouter() expect(screen.getByTestId('days-breakdown')).toBeInTheDocument() }) it('shows progress chart with real data', () => { renderWithRouter() expect(screen.getByTestId('progress-chart')).toBeInTheDocument() }) it('shows Swedish section headings', () => { renderWithRouter() expect(screen.getByTestId('metrics-title')).toHaveTextContent(/Översikt/) expect(screen.getAllByText(/Planerade dagar/)[0]).toBeInTheDocument() expect(screen.getByText(/Återstående till 4 års ålder/)).toBeInTheDocument() }) it('displays error message if analytics fails', () => { vi.mocked(hooks.useAnalytics).mockReturnValue({ data: null, isLoading: false, error: 'Failed to load analytics', refresh: vi.fn(), } as any) renderWithRouter() expect(screen.getByText(/Failed to load analytics/)).toBeInTheDocument() }) it('shows empty state for parental leave when data is null', () => { vi.mocked(hooks.useParentalLeave).mockReturnValue({ data: null, isLoading: false, error: null, refresh: vi.fn(), } as any) renderWithRouter() expect(screen.getByText(/Ingen data hämtad ännu/)).toBeInTheDocument() }) it('opens QR code modal on FK fetch button click', async () => { const user = userEvent.setup() const mockStart = vi.fn() vi.mocked(hooks.useScraperFetch).mockReturnValue({ sessionId: null, pollData: null, resolvedIds: null, error: null, isUnauthorized: false, start: mockStart, cancel: vi.fn(), isRunning: false, isTerminal: false, } as any) renderWithRouter() const button = screen.getByRole('button', { name: /Hämta från Försäkringskassan/ }) await user.click(button) expect(mockStart).toHaveBeenCalledWith('c-1') }) it('shows FK button is not disabled by default with child selected', () => { renderWithRouter() const button = screen.getByRole('button', { name: /Hämta från Försäkringskassan/ }) expect(button).toBeInTheDocument() expect(button).not.toBeDisabled() }) it('submits add child form with valid data', async () => { const user = userEvent.setup() const mockCreateChild = vi.fn().mockResolvedValue({ id: 'c-2', name: 'New Child', birth_date: '2024-01-15' }) const mockRefreshContext = vi.fn().mockResolvedValue(undefined) vi.mocked(useWorkspace).mockReturnValue({ ...mockWorkspaceContext, children: [], selectedChildId: null, refreshContext: mockRefreshContext, } as any) const mockApi = await import('@/services/api') vi.mocked(mockApi.createChild).mockImplementation(mockCreateChild) renderWithRouter() const nameInput = screen.getByLabelText(/Barnets namn/) const birthDateInput = screen.getByTestId('birth-date-input') const submitButton = screen.getByRole('button', { name: /Lägg till barn/ }) await user.type(nameInput, 'Test Child') await user.type(birthDateInput, '2024-01-15') await user.click(submitButton) expect(mockCreateChild).toHaveBeenCalledWith({ workspace_id: 'ws-1', name: 'Test Child', birth_date: '2024-01-15', }) expect(mockRefreshContext).toHaveBeenCalled() }) it('shows error when add child form submission fails', async () => { const user = userEvent.setup() const mockCreateChild = vi.fn().mockRejectedValue(new Error('API error')) vi.mocked(useWorkspace).mockReturnValue({ ...mockWorkspaceContext, children: [], selectedChildId: null, refreshContext: vi.fn(), } as any) const mockApi = await import('@/services/api') vi.mocked(mockApi.createChild).mockImplementation(mockCreateChild) renderWithRouter() const nameInput = screen.getByLabelText(/Barnets namn/) const birthDateInput = screen.getByTestId('birth-date-input') const submitButton = screen.getByRole('button', { name: /Lägg till barn/ }) await user.type(nameInput, 'Test Child') await user.type(birthDateInput, '2024-01-15') await user.click(submitButton) expect(screen.getByText(/API error/)).toBeInTheDocument() }) it('disables submit button during form submission', async () => { const user = userEvent.setup() const mockCreateChild = vi.fn().mockImplementation( () => new Promise(resolve => setTimeout(() => resolve({ id: 'c-2' }), 100)) ) vi.mocked(useWorkspace).mockReturnValue({ ...mockWorkspaceContext, children: [], selectedChildId: null, refreshContext: vi.fn().mockResolvedValue(undefined), } as any) const mockApi = await import('@/services/api') vi.mocked(mockApi.createChild).mockImplementation(mockCreateChild) renderWithRouter() const nameInput = screen.getByLabelText(/Barnets namn/) const birthDateInput = screen.getByTestId('birth-date-input') const submitButton = screen.getByRole('button', { name: /Lägg till barn/ }) await user.type(nameInput, 'Test Child') await user.type(birthDateInput, '2024-01-15') await user.click(submitButton) expect(submitButton).toBeDisabled() }) it('validates add child form with workspace check', async () => { vi.mocked(useWorkspace).mockReturnValue({ ...mockWorkspaceContext, children: [], selectedChildId: null, workspace: null, refreshContext: vi.fn(), } as any) renderWithRouter() const nameInput = screen.getByLabelText(/Barnets namn/) const birthDateInput = screen.getByTestId('birth-date-input') const user = userEvent.setup() await user.type(nameInput, 'Test Child') await user.type(birthDateInput, '2024-01-15') const submitButton = screen.getByRole('button', { name: /Lägg till barn/ }) await user.click(submitButton) expect(screen.getByText(/Fyll i alla fält/)).toBeInTheDocument() }) it('has manual fetch button enabled when child selected', () => { renderWithRouter() const manualButton = screen.getByRole('button', { name: /Ange uppgifter manuellt/ }) expect(manualButton).toBeInTheDocument() expect(manualButton).not.toBeDisabled() }) it('disables manual fetch button when no child selected', () => { vi.mocked(useWorkspace).mockReturnValue({ ...mockWorkspaceContext, selectedChildId: null, } as any) renderWithRouter() const manualButton = screen.getByRole('button', { name: /Ange uppgifter manuellt/ }) expect(manualButton).toBeDisabled() }) it('renders QRCodeModal when scraper is active', () => { vi.mocked(hooks.useScraperFetch).mockReturnValue({ sessionId: 'session1', pollData: { status: 'in_progress' }, resolvedIds: null, error: null, isUnauthorized: false, start: vi.fn(), cancel: vi.fn(), isRunning: true, isTerminal: false, } as any) renderWithRouter() // QRCodeModal is rendered inside Dashboard // Verify it was constructed with proper props expect(screen.getByText(/Hämta från Försäkringskassan/)).toBeInTheDocument() }) it('renders planned days breakdown with multiple day types', () => { vi.mocked(hooks.usePlannedDays).mockReturnValue({ data: [ { id: '1', workspaceId: 'ws-1', childId: 'c-1', parentPersonalDataUuid: 'pd-1', date: '2026-06-15', dayType: 'vacation', isDubbeldag: false, createdAt: '', updatedAt: '' }, { id: '2', workspaceId: 'ws-1', childId: 'c-1', parentPersonalDataUuid: 'pd-1', date: '2026-06-16', dayType: 'sjukpenning', isDubbeldag: false, createdAt: '', updatedAt: '' }, { id: '3', workspaceId: 'ws-1', childId: 'c-1', parentPersonalDataUuid: 'pd-1', date: '2026-06-17', dayType: 'lagsta', isDubbeldag: false, createdAt: '', updatedAt: '' }, ], isLoading: false, error: null, add: vi.fn(), remove: vi.fn(), } as any) renderWithRouter() expect(screen.getByTestId('days-breakdown')).toBeInTheDocument() }) it('shows analytics loading state', () => { vi.mocked(hooks.useAnalytics).mockReturnValue({ data: null, isLoading: true, error: null, refresh: vi.fn(), } as any) renderWithRouter() expect(screen.getByText(/Laddar/)).toBeInTheDocument() }) it('shows planned days loading state', () => { vi.mocked(hooks.usePlannedDays).mockReturnValue({ data: null, isLoading: true, error: null, add: vi.fn(), remove: vi.fn(), } as any) renderWithRouter() expect(screen.getByText(/Laddar/)).toBeInTheDocument() }) it('renders with zero planned days', () => { vi.mocked(hooks.usePlannedDays).mockReturnValue({ data: [], isLoading: false, error: null, add: vi.fn(), remove: vi.fn(), } as any) renderWithRouter() expect(screen.getByTestId('days-breakdown')).toBeInTheDocument() }) it('displays metrics with expiring days at B4', () => { vi.mocked(hooks.useAnalytics).mockReturnValue({ data: { workspaceId: 'ws-1', childId: 'c-1', parentProjections: [], combinedDaysUsed: 50, combinedDaysLeft: 200, combinedExpiringAtB4: 104, }, isLoading: false, error: null, refresh: vi.fn(), } as any) renderWithRouter() expect(screen.getByTestId('overview-metrics')).toBeInTheDocument() }) it('shows only the combined expiring figure for a single-parent workspace', () => { vi.mocked(hooks.useAnalytics).mockReturnValue({ data: { ...mockAnalyticsData, parentProjections: [ { personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 200, savableAfterB4: 96, expiringAtB4: 104 }, ], combinedExpiringAtB4: 104, }, isLoading: false, error: null, refresh: vi.fn(), } as any) renderWithRouter() expect(screen.getByTestId('metric-expiring-combined')).toBeInTheDocument() expect(screen.queryByTestId('metric-expiring-parent-pd-1')).not.toBeInTheDocument() }) it('breaks down expiring days per parent alongside the combined total', () => { vi.mocked(hooks.useAnalytics).mockReturnValue({ data: { ...mockAnalyticsData, parentProjections: [ { personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 200, savableAfterB4: 96, expiringAtB4: 60 }, { personalDataUuid: 'pd-2', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 20, plannedDaysBeforeB4: 5, projectedLeftAtB4: 210, savableAfterB4: 96, expiringAtB4: 44 }, ], combinedExpiringAtB4: 104, }, isLoading: false, error: null, refresh: vi.fn(), } as any) renderWithRouter() const alexMetric = screen.getByTestId('metric-expiring-parent-pd-1') const samMetric = screen.getByTestId('metric-expiring-parent-pd-2') const combinedMetric = screen.getByTestId('metric-expiring-combined') expect(alexMetric).toHaveTextContent('Alex') expect(alexMetric).toHaveTextContent('60') expect(samMetric).toHaveTextContent('Sam') expect(samMetric).toHaveTextContent('44') expect(combinedMetric).toHaveTextContent('104') }) it('falls back to a numbered parent label while members are still loading', () => { vi.mocked(hooks.useWorkspaceMembers).mockReturnValue({ data: [], isLoading: true, error: null, refresh: vi.fn(), } as any) vi.mocked(hooks.useAnalytics).mockReturnValue({ data: { ...mockAnalyticsData, parentProjections: [ { personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 200, savableAfterB4: 96, expiringAtB4: 60 }, { personalDataUuid: 'pd-2', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 20, plannedDaysBeforeB4: 5, projectedLeftAtB4: 210, savableAfterB4: 96, expiringAtB4: 44 }, ], combinedExpiringAtB4: 104, }, isLoading: false, error: null, refresh: vi.fn(), } as any) renderWithRouter() expect(screen.getByTestId('metric-expiring-parent-pd-1')).toHaveTextContent('Förälder 1') expect(screen.getByTestId('metric-expiring-parent-pd-2')).toHaveTextContent('Förälder 2') }) it('falls back to a numbered parent label for a projection with no matching member', () => { vi.mocked(hooks.useAnalytics).mockReturnValue({ data: { ...mockAnalyticsData, parentProjections: [ { personalDataUuid: 'pd-1', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 30, plannedDaysBeforeB4: 10, projectedLeftAtB4: 200, savableAfterB4: 96, expiringAtB4: 60 }, { personalDataUuid: 'pd-unknown', childId: 'c-1', birthDate: '2022-01-01', fourthBirthday: '2026-01-01', totalDays: 240, daysUsed: 20, plannedDaysBeforeB4: 5, projectedLeftAtB4: 210, savableAfterB4: 96, expiringAtB4: 44 }, ], combinedExpiringAtB4: 104, }, isLoading: false, error: null, refresh: vi.fn(), } as any) renderWithRouter() expect(screen.getByTestId('metric-expiring-parent-pd-1')).toHaveTextContent('Alex') expect(screen.getByTestId('metric-expiring-parent-pd-unknown')).toHaveTextContent('Förälder 2') }) })