Remove every as any from the test suite and enforce it
ESLint turned `no-explicit-any` off for test files, and 219 casts had accumulated across 12 of them. Two shapes, very different risks: most were `(useWorkspace as any).mockReturnValue(...)`, mere laziness that `vi.mocked()` does properly; the rest cast partial fixtures onto API types, which erases the contract exactly where a test should enforce it. That second kind matters here specifically. This project's recurring bug is a field silently dropped between backend and frontend -- the hand-written mapping in models/plan.py has done it twice, and apiFetch mishandling structured errors was the same shape. A test that casts an incomplete fixture to `any` keeps passing through precisely that change. A typed fixture layer already existed at tests/fixtures/mockData.ts and was imported by exactly zero tests: the right tool was there and invisible, so everyone reached for a cast. It is now a set of `make*(overrides: Partial<T>) => T` factories returning complete, valid objects, split by domain with a barrel. When a type gains a field, one factory fails to compile instead of thirty tests silently passing. With the count at zero the ESLint exemption is deleted, so this cannot regrow. Removing it immediately caught two `}: any` prop stubs that a grep for "as any" had missed; those mocks now use the real components' exported prop types. `as unknown as T` survives in five places, each a test deliberately feeding invalid input (an undefined breakdown, missing tierData, a bogus leave type). CLAUDE.md now documents that as the only accepted escape, and points at the factories. interface PollResponse in useScraperFetch.ts is exported so its shape can be derived rather than duplicated -- pollScraperSession declares Promise<unknown>, so there was no other honest way to type it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
This commit is contained in:
@@ -5,6 +5,15 @@ import { BrowserRouter } from 'react-router-dom'
|
||||
import { Dashboard } from './Dashboard'
|
||||
import * as hooks from '@/hooks'
|
||||
import { useWorkspace, useAuth } from '@/context'
|
||||
import {
|
||||
makeAuthContextValue,
|
||||
makeWorkspaceContextValue,
|
||||
makeAnalyticsResult,
|
||||
makeParentalLeaveResult,
|
||||
makePlannedDaysResult,
|
||||
makeScraperFetchResult,
|
||||
makeWorkspaceMembersResult,
|
||||
} from '../../tests/fixtures'
|
||||
|
||||
vi.mock('@/hooks')
|
||||
vi.mock('@/context')
|
||||
@@ -22,126 +31,106 @@ const renderWithRouter = (component: React.ReactElement) => {
|
||||
}
|
||||
|
||||
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)
|
||||
vi.mocked(useAuth).mockReturnValue(makeAuthContextValue())
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', planningGoal: 'balanced', planningMode: 'solo', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [{ id: 'c-1', name: 'Child 1', birth_date: '2022-01-01' }],
|
||||
selectedChildId: 'c-1',
|
||||
personalDataUuid: 'pd-1',
|
||||
parentName: 'Test Parent',
|
||||
})
|
||||
)
|
||||
vi.mocked(hooks.useAnalytics).mockReturnValue(
|
||||
makeAnalyticsResult({
|
||||
data: {
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
parentProjections: [],
|
||||
combinedDaysUsed: 30,
|
||||
combinedDaysLeft: 270,
|
||||
combinedExpiringAtB4: 100,
|
||||
},
|
||||
})
|
||||
)
|
||||
vi.mocked(hooks.useParentalLeave).mockReturnValue(
|
||||
makeParentalLeaveResult({
|
||||
data: {
|
||||
id: 'pf-1',
|
||||
personalDataUuid: 'pd-1',
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
source: 'scraped',
|
||||
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',
|
||||
},
|
||||
})
|
||||
)
|
||||
vi.mocked(hooks.usePlannedDays).mockReturnValue(
|
||||
makePlannedDaysResult({
|
||||
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: '' },
|
||||
],
|
||||
})
|
||||
)
|
||||
vi.mocked(hooks.useScraperFetch).mockReturnValue(makeScraperFetchResult())
|
||||
vi.mocked(hooks.useWorkspaceMembers).mockReturnValue(
|
||||
makeWorkspaceMembersResult({
|
||||
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' },
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('renders loading spinner initially', () => {
|
||||
vi.mocked(useWorkspace).mockReturnValue({
|
||||
...mockWorkspaceContext,
|
||||
isLoading: true,
|
||||
} as any)
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', planningGoal: 'balanced', planningMode: 'solo', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [{ id: 'c-1', name: 'Child 1', birth_date: '2022-01-01' }],
|
||||
selectedChildId: 'c-1',
|
||||
personalDataUuid: 'pd-1',
|
||||
parentName: 'Test Parent',
|
||||
isLoading: true,
|
||||
})
|
||||
)
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByText(/Laddar/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows add child form when no children', () => {
|
||||
vi.mocked(useWorkspace).mockReturnValue({
|
||||
...mockWorkspaceContext,
|
||||
children: [],
|
||||
selectedChildId: null,
|
||||
} as any)
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', planningGoal: 'balanced', planningMode: 'solo', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [],
|
||||
selectedChildId: null,
|
||||
personalDataUuid: 'pd-1',
|
||||
parentName: 'Test Parent',
|
||||
})
|
||||
)
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByText(/Du har inga barn registrerade ännu/)).toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/Barnets namn/)).toBeInTheDocument()
|
||||
@@ -169,10 +158,16 @@ describe('Dashboard', () => {
|
||||
})
|
||||
|
||||
it('disables FK fetch button when no child selected', () => {
|
||||
vi.mocked(useWorkspace).mockReturnValue({
|
||||
...mockWorkspaceContext,
|
||||
selectedChildId: null,
|
||||
} as any)
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', planningGoal: 'balanced', planningMode: 'solo', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [{ id: 'c-1', name: 'Child 1', birth_date: '2022-01-01' }],
|
||||
selectedChildId: null,
|
||||
personalDataUuid: 'pd-1',
|
||||
parentName: 'Test Parent',
|
||||
})
|
||||
)
|
||||
renderWithRouter(<Dashboard />)
|
||||
const button = screen.getByRole('button', { name: /Hämta från Försäkringskassan/ })
|
||||
expect(button).toBeDisabled()
|
||||
@@ -213,19 +208,21 @@ describe('Dashboard', () => {
|
||||
})
|
||||
|
||||
it('labels the per-parent expiring-at-4 metric as reflecting the current saved plan', () => {
|
||||
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)
|
||||
vi.mocked(hooks.useAnalytics).mockReturnValue(
|
||||
makeAnalyticsResult({
|
||||
data: {
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
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 },
|
||||
],
|
||||
combinedDaysUsed: 50,
|
||||
combinedDaysLeft: 190,
|
||||
combinedExpiringAtB4: 104,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByTestId('metric-expiring-parent-pd-1')).toHaveTextContent(
|
||||
@@ -234,23 +231,22 @@ describe('Dashboard', () => {
|
||||
})
|
||||
|
||||
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)
|
||||
vi.mocked(hooks.useAnalytics).mockReturnValue(
|
||||
makeAnalyticsResult({
|
||||
data: null,
|
||||
error: 'Failed to load analytics',
|
||||
})
|
||||
)
|
||||
renderWithRouter(<Dashboard />)
|
||||
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)
|
||||
vi.mocked(hooks.useParentalLeave).mockReturnValue(
|
||||
makeParentalLeaveResult({
|
||||
data: null,
|
||||
})
|
||||
)
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByText(/Ingen data hämtad ännu/)).toBeInTheDocument()
|
||||
})
|
||||
@@ -258,17 +254,11 @@ describe('Dashboard', () => {
|
||||
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)
|
||||
vi.mocked(hooks.useScraperFetch).mockReturnValue(
|
||||
makeScraperFetchResult({
|
||||
start: mockStart,
|
||||
})
|
||||
)
|
||||
|
||||
renderWithRouter(<Dashboard />)
|
||||
const button = screen.getByRole('button', { name: /Hämta från Försäkringskassan/ })
|
||||
@@ -289,12 +279,17 @@ describe('Dashboard', () => {
|
||||
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)
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', planningGoal: 'balanced', planningMode: 'solo', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [],
|
||||
selectedChildId: null,
|
||||
personalDataUuid: 'pd-1',
|
||||
parentName: 'Test Parent',
|
||||
refreshContext: mockRefreshContext,
|
||||
})
|
||||
)
|
||||
|
||||
const mockApi = await import('@/services/api')
|
||||
vi.mocked(mockApi.createChild).mockImplementation(mockCreateChild)
|
||||
@@ -321,12 +316,16 @@ describe('Dashboard', () => {
|
||||
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)
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', planningGoal: 'balanced', planningMode: 'solo', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [],
|
||||
selectedChildId: null,
|
||||
personalDataUuid: 'pd-1',
|
||||
parentName: 'Test Parent',
|
||||
})
|
||||
)
|
||||
|
||||
const mockApi = await import('@/services/api')
|
||||
vi.mocked(mockApi.createChild).mockImplementation(mockCreateChild)
|
||||
@@ -350,12 +349,17 @@ describe('Dashboard', () => {
|
||||
() => new Promise(resolve => setTimeout(() => resolve({ id: 'c-2' }), 100))
|
||||
)
|
||||
|
||||
vi.mocked(useWorkspace).mockReturnValue({
|
||||
...mockWorkspaceContext,
|
||||
children: [],
|
||||
selectedChildId: null,
|
||||
refreshContext: vi.fn().mockResolvedValue(undefined),
|
||||
} as any)
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', planningGoal: 'balanced', planningMode: 'solo', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [],
|
||||
selectedChildId: null,
|
||||
personalDataUuid: 'pd-1',
|
||||
parentName: 'Test Parent',
|
||||
refreshContext: vi.fn().mockResolvedValue(undefined),
|
||||
})
|
||||
)
|
||||
|
||||
const mockApi = await import('@/services/api')
|
||||
vi.mocked(mockApi.createChild).mockImplementation(mockCreateChild)
|
||||
@@ -374,13 +378,13 @@ describe('Dashboard', () => {
|
||||
})
|
||||
|
||||
it('validates add child form with workspace check', async () => {
|
||||
vi.mocked(useWorkspace).mockReturnValue({
|
||||
...mockWorkspaceContext,
|
||||
children: [],
|
||||
selectedChildId: null,
|
||||
workspace: null,
|
||||
refreshContext: vi.fn(),
|
||||
} as any)
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: null,
|
||||
children: [],
|
||||
selectedChildId: null,
|
||||
})
|
||||
)
|
||||
|
||||
renderWithRouter(<Dashboard />)
|
||||
|
||||
@@ -406,10 +410,16 @@ describe('Dashboard', () => {
|
||||
})
|
||||
|
||||
it('disables manual fetch button when no child selected', () => {
|
||||
vi.mocked(useWorkspace).mockReturnValue({
|
||||
...mockWorkspaceContext,
|
||||
selectedChildId: null,
|
||||
} as any)
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', planningGoal: 'balanced', planningMode: 'solo', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [{ id: 'c-1', name: 'Child 1', birth_date: '2022-01-01' }],
|
||||
selectedChildId: null,
|
||||
personalDataUuid: 'pd-1',
|
||||
parentName: 'Test Parent',
|
||||
})
|
||||
)
|
||||
renderWithRouter(<Dashboard />)
|
||||
|
||||
const manualButton = screen.getByRole('button', { name: /Ange uppgifter manuellt/ })
|
||||
@@ -417,17 +427,13 @@ describe('Dashboard', () => {
|
||||
})
|
||||
|
||||
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)
|
||||
vi.mocked(hooks.useScraperFetch).mockReturnValue(
|
||||
makeScraperFetchResult({
|
||||
sessionId: 'session1',
|
||||
pollData: { status: 'authenticating' },
|
||||
isRunning: true,
|
||||
})
|
||||
)
|
||||
|
||||
renderWithRouter(<Dashboard />)
|
||||
// QRCodeModal is rendered inside Dashboard
|
||||
@@ -436,90 +442,86 @@ describe('Dashboard', () => {
|
||||
})
|
||||
|
||||
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)
|
||||
vi.mocked(hooks.usePlannedDays).mockReturnValue(
|
||||
makePlannedDaysResult({
|
||||
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: '' },
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
renderWithRouter(<Dashboard />)
|
||||
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)
|
||||
vi.mocked(hooks.useAnalytics).mockReturnValue(
|
||||
makeAnalyticsResult({
|
||||
data: null,
|
||||
isLoading: true,
|
||||
})
|
||||
)
|
||||
renderWithRouter(<Dashboard />)
|
||||
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)
|
||||
vi.mocked(hooks.usePlannedDays).mockReturnValue(
|
||||
makePlannedDaysResult({
|
||||
data: null,
|
||||
isLoading: true,
|
||||
})
|
||||
)
|
||||
renderWithRouter(<Dashboard />)
|
||||
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)
|
||||
vi.mocked(hooks.usePlannedDays).mockReturnValue(
|
||||
makePlannedDaysResult({
|
||||
data: [],
|
||||
})
|
||||
)
|
||||
|
||||
renderWithRouter(<Dashboard />)
|
||||
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)
|
||||
vi.mocked(hooks.useAnalytics).mockReturnValue(
|
||||
makeAnalyticsResult({
|
||||
data: {
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
parentProjections: [],
|
||||
combinedDaysUsed: 50,
|
||||
combinedDaysLeft: 200,
|
||||
combinedExpiringAtB4: 104,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
renderWithRouter(<Dashboard />)
|
||||
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)
|
||||
vi.mocked(hooks.useAnalytics).mockReturnValue(
|
||||
makeAnalyticsResult({
|
||||
data: {
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
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 },
|
||||
],
|
||||
combinedDaysUsed: 30,
|
||||
combinedDaysLeft: 210,
|
||||
combinedExpiringAtB4: 104,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByTestId('metric-expiring-combined')).toBeInTheDocument()
|
||||
@@ -527,19 +529,21 @@ describe('Dashboard', () => {
|
||||
})
|
||||
|
||||
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)
|
||||
vi.mocked(hooks.useAnalytics).mockReturnValue(
|
||||
makeAnalyticsResult({
|
||||
data: {
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
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 },
|
||||
],
|
||||
combinedDaysUsed: 50,
|
||||
combinedDaysLeft: 190,
|
||||
combinedExpiringAtB4: 104,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
renderWithRouter(<Dashboard />)
|
||||
const alexMetric = screen.getByTestId('metric-expiring-parent-pd-1')
|
||||
@@ -553,25 +557,27 @@ describe('Dashboard', () => {
|
||||
})
|
||||
|
||||
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)
|
||||
vi.mocked(hooks.useWorkspaceMembers).mockReturnValue(
|
||||
makeWorkspaceMembersResult({
|
||||
data: [],
|
||||
isLoading: true,
|
||||
})
|
||||
)
|
||||
vi.mocked(hooks.useAnalytics).mockReturnValue(
|
||||
makeAnalyticsResult({
|
||||
data: {
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
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 },
|
||||
],
|
||||
combinedDaysUsed: 50,
|
||||
combinedDaysLeft: 190,
|
||||
combinedExpiringAtB4: 104,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByTestId('metric-expiring-parent-pd-1')).toHaveTextContent('Förälder 1')
|
||||
@@ -579,19 +585,21 @@ describe('Dashboard', () => {
|
||||
})
|
||||
|
||||
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)
|
||||
vi.mocked(hooks.useAnalytics).mockReturnValue(
|
||||
makeAnalyticsResult({
|
||||
data: {
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
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 },
|
||||
],
|
||||
combinedDaysUsed: 50,
|
||||
combinedDaysLeft: 190,
|
||||
combinedExpiringAtB4: 104,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
renderWithRouter(<Dashboard />)
|
||||
expect(screen.getByTestId('metric-expiring-parent-pd-1')).toHaveTextContent('Alex')
|
||||
|
||||
Reference in New Issue
Block a user