Redesign the setup flow and consolidate the CSS into one primitives layer
Onboarding asks explicitly for one or two parents and shows one card per parent for balances; the balance form is per parent with inline validation; the partner can be filled from the FK deduction or added from it. Settings and Dashboard reuse the same form. CSS: bare buttons carry no colour any more; buttons, cards, choice cards, fields, alerts, badges, eyebrow and modal chrome are defined once in primitives.css. Media queries live only in responsive.css. A style guard test blocks hex, !important and stray media queries. Bilingual fixes: calendar selects size by content, header no longer wraps. Vitest is capped at four workers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { Dashboard } from './Dashboard'
|
||||
import * as hooks from '@/hooks'
|
||||
import * as api from '@/services/api'
|
||||
import { useWorkspace, useAuth } from '@/context'
|
||||
import {
|
||||
makeAuthContextValue,
|
||||
@@ -17,6 +18,8 @@ import {
|
||||
makeOptimizationTipsResult,
|
||||
makeLeaveWishesResult,
|
||||
makePlanProposalResult,
|
||||
makeParentalLeaveFetch,
|
||||
makeTierData,
|
||||
makeTip,
|
||||
} from '../../tests/fixtures'
|
||||
|
||||
@@ -54,7 +57,7 @@ describe('Dashboard', () => {
|
||||
vi.mocked(useAuth).mockReturnValue(makeAuthContextValue())
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', planningMode: 'solo', createdAt: '2026-01-01' },
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [{ id: 'c-1', name: 'Child 1', birth_date: '2022-01-01' }],
|
||||
selectedChildId: 'c-1',
|
||||
@@ -123,12 +126,17 @@ describe('Dashboard', () => {
|
||||
vi.mocked(hooks.useOptimizationTips).mockReturnValue(makeOptimizationTipsResult())
|
||||
vi.mocked(hooks.useLeaveWishes).mockReturnValue(makeLeaveWishesResult())
|
||||
vi.mocked(hooks.usePlanProposal).mockReturnValue(makePlanProposalResult())
|
||||
// Default: nothing saved yet for whoever's balance form gets opened —
|
||||
// `useLatestBalance` (a real hook, imported directly by Dashboard.tsx to
|
||||
// bypass the `vi.mock('@/hooks')` above) falls back to the form's
|
||||
// preset. Tests that care about a saved balance override this.
|
||||
vi.mocked(api.getParentalLeave).mockResolvedValue(null)
|
||||
})
|
||||
|
||||
it('renders loading spinner initially', () => {
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', planningMode: 'solo', createdAt: '2026-01-01' },
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [{ id: 'c-1', name: 'Child 1', birth_date: '2022-01-01' }],
|
||||
selectedChildId: 'c-1',
|
||||
@@ -144,7 +152,7 @@ describe('Dashboard', () => {
|
||||
it('hands off to Onboarding when there are no children yet', () => {
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', planningMode: 'solo', createdAt: '2026-01-01' },
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [],
|
||||
selectedChildId: null,
|
||||
@@ -170,7 +178,7 @@ describe('Dashboard', () => {
|
||||
makeWorkspaceMembersResult({ refresh: refreshMembers, data: [] }),
|
||||
)
|
||||
const soloWorkspace = makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', planningMode: 'couple', createdAt: '2026-01-01' },
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [],
|
||||
selectedChildId: null,
|
||||
@@ -349,7 +357,7 @@ describe('Dashboard', () => {
|
||||
it('disables FK fetch button when no child selected', () => {
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', planningMode: 'solo', createdAt: '2026-01-01' },
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [{ id: 'c-1', name: 'Child 1', birth_date: '2022-01-01' }],
|
||||
selectedChildId: null,
|
||||
@@ -468,7 +476,7 @@ describe('Dashboard', () => {
|
||||
it('disables manual fetch button when no child selected', () => {
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', planningMode: 'solo', createdAt: '2026-01-01' },
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [{ id: 'c-1', name: 'Child 1', birth_date: '2022-01-01' }],
|
||||
selectedChildId: null,
|
||||
@@ -661,4 +669,97 @@ describe('Dashboard', () => {
|
||||
expect(screen.getByTestId('metric-expiring-parent-pd-1')).toHaveTextContent('Alex')
|
||||
expect(screen.getByTestId('metric-expiring-parent-pd-unknown')).toHaveTextContent('Förälder 2')
|
||||
})
|
||||
|
||||
describe('manual balance entry — parent picker', () => {
|
||||
it('shows a parent picker before the form when the workspace has two members', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWithRouter(<Dashboard />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Ange uppgifter manuellt/ }))
|
||||
|
||||
expect(screen.getByTestId('dashboard-balance-parent-picker')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('dashboard-balance-parent-pd-1')).toHaveTextContent('Alex')
|
||||
expect(screen.getByTestId('dashboard-balance-parent-pd-2')).toHaveTextContent('Sam')
|
||||
})
|
||||
|
||||
it('opens the form directly, with no picker, for a solo workspace', async () => {
|
||||
vi.mocked(hooks.useWorkspaceMembers).mockReturnValue(
|
||||
makeWorkspaceMembersResult({
|
||||
data: [{ uuid: 'pd-1', workspaceId: 'ws-1', name: 'Alex', salaryType: 'monthly', employmentStatus: 'employed', createdAt: '2026-01-01' }],
|
||||
})
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
renderWithRouter(<Dashboard />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Ange uppgifter manuellt/ }))
|
||||
|
||||
expect(screen.queryByTestId('dashboard-balance-parent-picker')).not.toBeInTheDocument()
|
||||
expect(await screen.findByText('Dina dagar kvar')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('posts the chosen parent\'s uuid to saveManualFetch', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWithRouter(<Dashboard />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Ange uppgifter manuellt/ }))
|
||||
await user.click(screen.getByTestId('dashboard-balance-parent-pd-2'))
|
||||
expect(screen.queryByTestId('dashboard-balance-parent-picker')).not.toBeInTheDocument()
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'Spara' }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(api.saveManualFetch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ personalDataUuid: 'pd-2', workspaceId: 'ws-1', childId: 'c-1', source: 'manual' })
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('shows an error when saving a manual balance fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(api.saveManualFetch).mockRejectedValue(new Error('save failed'))
|
||||
renderWithRouter(<Dashboard />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Ange uppgifter manuellt/ }))
|
||||
await user.click(screen.getByTestId('dashboard-balance-parent-pd-1'))
|
||||
await user.click(await screen.findByRole('button', { name: 'Spara' }))
|
||||
|
||||
expect(await screen.findByText('save failed')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('prefills the manual-entry form with the parent\'s saved balance', async () => {
|
||||
vi.mocked(api.getParentalLeave).mockResolvedValue(
|
||||
makeParentalLeaveFetch({
|
||||
personalDataUuid: 'pd-2',
|
||||
childId: 'c-1',
|
||||
dubbeldagarLeft: 7,
|
||||
tierData: makeTierData({ sjukpenningLeft: 65, lagstaNivaLeft: 8 }),
|
||||
}),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
renderWithRouter(<Dashboard />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Ange uppgifter manuellt/ }))
|
||||
await user.click(screen.getByTestId('dashboard-balance-parent-pd-2'))
|
||||
|
||||
expect(await screen.findByText('Sams dagar kvar')).toBeInTheDocument()
|
||||
expect((screen.getByLabelText(/Sjukpenningnivå/) as HTMLInputElement).value).toBe('65')
|
||||
expect((screen.getByLabelText(/Lägstanivå/) as HTMLInputElement).value).toBe('8')
|
||||
expect((screen.getByLabelText(/Dubbeldagar/) as HTMLInputElement).value).toBe('7')
|
||||
})
|
||||
|
||||
it('falls back to the preset when the parent has no saved balance yet', async () => {
|
||||
vi.mocked(api.getParentalLeave).mockResolvedValue(null)
|
||||
const user = userEvent.setup()
|
||||
renderWithRouter(<Dashboard />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Ange uppgifter manuellt/ }))
|
||||
await user.click(screen.getByTestId('dashboard-balance-parent-pd-1'))
|
||||
|
||||
expect(await screen.findByText('Dina dagar kvar')).toBeInTheDocument()
|
||||
// Couple workspace -> 240-day preset, split 195:45.
|
||||
expect((screen.getByLabelText(/Sjukpenningnivå/) as HTMLInputElement).value).toBe('195')
|
||||
expect((screen.getByLabelText(/Lägstanivå/) as HTMLInputElement).value).toBe('45')
|
||||
expect((screen.getByLabelText(/Dubbeldagar/) as HTMLInputElement).value).toBe('60')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user