76b1e712b9
Works through every numbered item in NextSteps260913.md section 3, plus the three known verification issues in section 4.1. Surface what the backend already computed: - 3.1 render surplus_advice in PlanResults, mirroring the shortfall remedies - 3.2 surface VacationBalance.is_assumed so assumed statutory years are visible - 3.3 age-4 expiry given the proposed plan, via a new Age4Projection reusing the existing _age4_projections arithmetic (Dashboard stat verified, not rebuilt) Calendar cluster (one pass over the holiday data): - new GET /api/calendar/days exposing roda dagar, klamdagar and day overrides, paginated four years at a time - 3.4 shade roda dagar/klamdagar, with R/K badges so colour is not the only signal - 3.6 click-to-override behind an explicit mode toggle, leaving drag multi-select intact - 3.7 click month/year to advance - 3.8 per-year red-days list in Settings, statutory days plus overrides i18n: - 3.5 five backend-generated Swedish surfaces converted to message_key + data, following the tips_service pattern; old string fields replaced, not kept Flexible wishes (3.9): - PLANNING_ENGINE_SPEC section 10: window_mode, candidate search, cost function, deterministic tie-break, two-pass ordering, test matrix rows 15-24 - backend: entity, migration 006, _FULL_SCHEMA sync, repository, allocator - frontend: season-preset toggle in WishEditor, unplaced outcome rendering Fixes (4.1): - pin typecheck to the local tsc rather than npx resolving a newer TypeScript - ignore generated coverage/ and dist/ in the ESLint flat config - fix the flaky usePlanApply test properly, with waitFor rather than a retry Tests: backend 361 -> 404, frontend 540 -> 704. Coverage 89.47% -> 96.52% lines. Not yet verified in a browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
132 lines
4.0 KiB
TypeScript
132 lines
4.0 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { render, screen } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { MemoryRouter } from 'react-router-dom'
|
|
import { Layout } from './Layout'
|
|
import { useAuth } from '@/context/AuthContext'
|
|
import { useWorkspace } from '@/context/WorkspaceContext'
|
|
|
|
// Layout imports useAuth/useWorkspace directly from their modules (not the
|
|
// @/context barrel), so those exact modules must be mocked here.
|
|
vi.mock('@/context/AuthContext')
|
|
vi.mock('@/context/WorkspaceContext')
|
|
|
|
const mockNavigate = vi.fn()
|
|
vi.mock('react-router-dom', async () => {
|
|
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom')
|
|
return {
|
|
...actual,
|
|
useNavigate: () => mockNavigate,
|
|
}
|
|
})
|
|
|
|
const mockUseAuth = vi.mocked(useAuth)
|
|
const mockUseWorkspace = vi.mocked(useWorkspace)
|
|
|
|
function renderLayout(children = <div>page content</div>) {
|
|
return render(
|
|
<MemoryRouter>
|
|
<Layout>{children}</Layout>
|
|
</MemoryRouter>,
|
|
)
|
|
}
|
|
|
|
describe('Layout', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockUseAuth.mockReturnValue({
|
|
accessToken: 'token',
|
|
isAuthenticated: true,
|
|
isLoading: false,
|
|
login: vi.fn(),
|
|
register: vi.fn(),
|
|
logout: vi.fn().mockResolvedValue(undefined),
|
|
})
|
|
mockUseWorkspace.mockReturnValue({
|
|
workspace: null,
|
|
isLoading: false,
|
|
error: null,
|
|
workspaceId: 'ws1',
|
|
setWorkspaceId: vi.fn(),
|
|
refetch: vi.fn(),
|
|
children: [],
|
|
selectedChildId: null,
|
|
selectChild: vi.fn(),
|
|
personalDataUuid: null,
|
|
parentName: null,
|
|
refreshContext: vi.fn(),
|
|
})
|
|
})
|
|
|
|
it('renders children inside the main content area', () => {
|
|
renderLayout(<div data-testid="page-content">hello</div>)
|
|
expect(screen.getByTestId('layout-main')).toContainElement(screen.getByTestId('page-content'))
|
|
})
|
|
|
|
it('renders Header and Footer', () => {
|
|
renderLayout()
|
|
expect(screen.getByTestId('header')).toBeInTheDocument()
|
|
expect(screen.getByTestId('footer')).toBeInTheDocument()
|
|
})
|
|
|
|
it('passes parent name and children list from workspace context to Header', () => {
|
|
mockUseWorkspace.mockReturnValue({
|
|
workspace: null,
|
|
isLoading: false,
|
|
error: null,
|
|
workspaceId: 'ws1',
|
|
setWorkspaceId: vi.fn(),
|
|
refetch: vi.fn(),
|
|
children: [
|
|
{ id: 'c1', name: 'Emma', birth_date: '2024-01-01' },
|
|
{ id: 'c2', name: 'Oscar', birth_date: '2025-01-01' },
|
|
],
|
|
selectedChildId: 'c1',
|
|
selectChild: vi.fn(),
|
|
personalDataUuid: 'pd1',
|
|
parentName: 'Elin',
|
|
refreshContext: vi.fn(),
|
|
})
|
|
renderLayout()
|
|
expect(screen.getByTestId('parent-name')).toHaveTextContent('Elin')
|
|
expect(screen.getByTestId('child-select')).toBeInTheDocument()
|
|
expect(screen.getByDisplayValue('Emma')).toBeInTheDocument()
|
|
})
|
|
|
|
it('selects the correct child when selectedChildId matches', () => {
|
|
const selectChild = vi.fn()
|
|
mockUseWorkspace.mockReturnValue({
|
|
workspace: null,
|
|
isLoading: false,
|
|
error: null,
|
|
workspaceId: 'ws1',
|
|
setWorkspaceId: vi.fn(),
|
|
refetch: vi.fn(),
|
|
children: [{ id: 'c1', name: 'Emma', birth_date: '2024-01-01' }],
|
|
selectedChildId: 'c1',
|
|
selectChild,
|
|
personalDataUuid: 'pd1',
|
|
parentName: null,
|
|
refreshContext: vi.fn(),
|
|
})
|
|
renderLayout()
|
|
expect(screen.getByDisplayValue('Emma')).toBeInTheDocument()
|
|
})
|
|
|
|
it('logs out and navigates to /auth when the logout button is clicked', async () => {
|
|
const logout = vi.fn().mockResolvedValue(undefined)
|
|
mockUseAuth.mockReturnValue({
|
|
accessToken: 'token',
|
|
isAuthenticated: true,
|
|
isLoading: false,
|
|
login: vi.fn(),
|
|
register: vi.fn(),
|
|
logout,
|
|
})
|
|
renderLayout()
|
|
await userEvent.click(screen.getByTestId('logout-button'))
|
|
expect(logout).toHaveBeenCalled()
|
|
expect(mockNavigate).toHaveBeenCalledWith('/auth', { replace: true })
|
|
})
|
|
})
|