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:
@@ -14,6 +14,8 @@ import {
|
||||
makeChildSettingsResult,
|
||||
makeDayOverridesResult,
|
||||
makeCalendarDaysResult,
|
||||
makeParentalLeaveFetch,
|
||||
makeTierData,
|
||||
} from '../../tests/fixtures'
|
||||
|
||||
vi.mock('@/hooks')
|
||||
@@ -117,7 +119,6 @@ describe('Settings', () => {
|
||||
workspace: {
|
||||
id: 'ws-1',
|
||||
name: 'Test Workspace',
|
||||
planningMode: 'solo',
|
||||
createdAt: '2026-01-01',
|
||||
},
|
||||
workspaceId: 'ws-1',
|
||||
@@ -160,12 +161,6 @@ describe('Settings', () => {
|
||||
data: mockCalendarWindow,
|
||||
})
|
||||
)
|
||||
vi.mocked(api.updateWorkspace).mockResolvedValue({
|
||||
id: 'ws-1',
|
||||
name: 'Test Workspace',
|
||||
planningMode: 'solo',
|
||||
createdAt: '2026-01-01',
|
||||
})
|
||||
vi.mocked(api.updateParentSettings).mockResolvedValue({
|
||||
uuid: 'pd-1',
|
||||
name: 'Test Parent',
|
||||
@@ -180,6 +175,11 @@ describe('Settings', () => {
|
||||
})
|
||||
vi.mocked(api.renameWorkspaceParent).mockResolvedValue(partnerMember)
|
||||
vi.mocked(api.removeWorkspaceParent).mockResolvedValue(undefined)
|
||||
// Default: nothing saved yet for whoever's balance form gets opened —
|
||||
// `useLatestBalance` (a real hook, imported directly by Settings.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 while workspace loads', () => {
|
||||
@@ -188,7 +188,6 @@ describe('Settings', () => {
|
||||
workspace: {
|
||||
id: 'ws-1',
|
||||
name: 'Test Workspace',
|
||||
planningMode: 'solo',
|
||||
createdAt: '2026-01-01',
|
||||
},
|
||||
workspaceId: 'ws-1',
|
||||
@@ -207,12 +206,24 @@ describe('Settings', () => {
|
||||
renderSettings()
|
||||
expect(screen.getByRole('heading', { name: 'Barn' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: 'Din profil' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: 'Planering' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: 'Partner' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: 'Semesterdagar' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: 'Kalender' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: 'Undantag i kalendern' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: 'Röda dagar' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: 'Partner' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the partner section where the removed planning-mode selector used to be, right after the profile section, and never renders a planning-mode selector', () => {
|
||||
renderSettings()
|
||||
const headings = screen.getAllByRole('heading', { level: 2 }).map((h) => h.textContent)
|
||||
const profileIndex = headings.indexOf('Din profil')
|
||||
const partnerIndex = headings.indexOf('Partner')
|
||||
const vacationIndex = headings.indexOf('Semesterdagar')
|
||||
expect(profileIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(partnerIndex).toBe(profileIndex + 1)
|
||||
expect(vacationIndex).toBe(partnerIndex + 1)
|
||||
expect(screen.queryByTestId('planning-mode-selector')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('heading', { name: 'Planering' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
describe('vacation days table', () => {
|
||||
@@ -274,25 +285,6 @@ describe('Settings', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('planning mode', () => {
|
||||
it('reflects the workspace planning mode', () => {
|
||||
renderSettings()
|
||||
expect(screen.getByTestId('planning-mode-solo')).toBeChecked()
|
||||
})
|
||||
|
||||
it('saves the selected planning mode', async () => {
|
||||
renderSettings()
|
||||
fireEvent.click(screen.getByTestId('planning-mode-couple'))
|
||||
fireEvent.click(screen.getByTestId('save-planning-goal-button'))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(api.updateWorkspace).toHaveBeenCalledWith('ws-1', {
|
||||
planningMode: 'couple',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('child settings', () => {
|
||||
it('prefills birth date and preschool planning days from the child', () => {
|
||||
renderSettings()
|
||||
@@ -356,6 +348,104 @@ describe('Settings', () => {
|
||||
expect(message).toHaveTextContent('Partnerns namn är ogiltigt.')
|
||||
})
|
||||
|
||||
describe('parent balance ("Dagar kvar")', () => {
|
||||
it('opens the balance form for a solo workspace with a 480-day total', async () => {
|
||||
renderSettings()
|
||||
fireEvent.click(screen.getByTestId('settings-balance-pd-1'))
|
||||
expect(await screen.findByText('Dina dagar kvar')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('prefills the form with the parent\'s saved balance instead of the preset', async () => {
|
||||
vi.mocked(api.getParentalLeave).mockResolvedValue(
|
||||
makeParentalLeaveFetch({
|
||||
personalDataUuid: 'pd-1',
|
||||
childId: 'c-1',
|
||||
dubbeldagarLeft: 12,
|
||||
reservedDaysLeft: 34,
|
||||
tierData: makeTierData({ sjukpenningLeft: 111, lagstaNivaLeft: 22 }),
|
||||
}),
|
||||
)
|
||||
renderSettings()
|
||||
|
||||
fireEvent.click(screen.getByTestId('settings-balance-pd-1'))
|
||||
|
||||
expect(await screen.findByText('Dina dagar kvar')).toBeInTheDocument()
|
||||
expect((screen.getByLabelText(/Sjukpenningnivå/) as HTMLInputElement).value).toBe('111')
|
||||
expect((screen.getByLabelText(/Lägstanivå/) as HTMLInputElement).value).toBe('22')
|
||||
expect((screen.getByLabelText(/Dubbeldagar/) as HTMLInputElement).value).toBe('12')
|
||||
})
|
||||
|
||||
it('falls back to the preset when the parent has no saved balance yet', async () => {
|
||||
vi.mocked(api.getParentalLeave).mockResolvedValue(null)
|
||||
renderSettings()
|
||||
|
||||
fireEvent.click(screen.getByTestId('settings-balance-pd-1'))
|
||||
|
||||
expect(await screen.findByText('Dina dagar kvar')).toBeInTheDocument()
|
||||
// Solo workspace -> 480-day preset, split 390:90.
|
||||
expect((screen.getByLabelText(/Sjukpenningnivå/) as HTMLInputElement).value).toBe('390')
|
||||
expect((screen.getByLabelText(/Lägstanivå/) as HTMLInputElement).value).toBe('90')
|
||||
})
|
||||
|
||||
it("falls back to the workspace's first child when no child is selected yet", async () => {
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [{ id: 'c-9', name: 'Child 9', birth_date: CHILD_BIRTH_DATE }],
|
||||
selectedChildId: null,
|
||||
personalDataUuid: 'pd-1',
|
||||
parentName: 'Test Parent',
|
||||
})
|
||||
)
|
||||
renderSettings()
|
||||
|
||||
fireEvent.click(screen.getByTestId('settings-balance-pd-1'))
|
||||
await screen.findByText('Dina dagar kvar')
|
||||
const saveButtons = screen.getAllByRole('button', { name: 'Spara' })
|
||||
fireEvent.click(saveButtons[saveButtons.length - 1]!)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(api.saveManualFetch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ personalDataUuid: 'pd-1', workspaceId: 'ws-1', childId: 'c-9', source: 'manual' })
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('does nothing when there is no child to attach the balance to', async () => {
|
||||
vi.mocked(useWorkspace).mockReturnValue(
|
||||
makeWorkspaceContextValue({
|
||||
workspace: { id: 'ws-1', name: 'Test Workspace', createdAt: '2026-01-01' },
|
||||
workspaceId: 'ws-1',
|
||||
children: [],
|
||||
selectedChildId: null,
|
||||
personalDataUuid: 'pd-1',
|
||||
parentName: 'Test Parent',
|
||||
})
|
||||
)
|
||||
renderSettings()
|
||||
|
||||
fireEvent.click(screen.getByTestId('settings-balance-pd-1'))
|
||||
await screen.findByText('Dina dagar kvar')
|
||||
const saveButtons = screen.getAllByRole('button', { name: 'Spara' })
|
||||
fireEvent.click(saveButtons[saveButtons.length - 1]!)
|
||||
|
||||
expect(api.saveManualFetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows an error when saving a balance fails', async () => {
|
||||
vi.mocked(api.saveManualFetch).mockRejectedValue(new Error('balance save failed'))
|
||||
renderSettings()
|
||||
|
||||
fireEvent.click(screen.getByTestId('settings-balance-pd-1'))
|
||||
await screen.findByText('Dina dagar kvar')
|
||||
const saveButtons = screen.getAllByRole('button', { name: 'Spara' })
|
||||
fireEvent.click(saveButtons[saveButtons.length - 1]!)
|
||||
|
||||
expect(await screen.findByText('balance save failed')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('workspace members', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(hooks.useWorkspaceMembers).mockReturnValue(
|
||||
@@ -497,6 +587,56 @@ describe('Settings', () => {
|
||||
const message = await screen.findByTestId('member-action-error-pd-2')
|
||||
expect(message).not.toHaveTextContent('workspace.parents.remove.self')
|
||||
})
|
||||
|
||||
it('opens the balance form for the clicked member and saves with that member\'s uuid', async () => {
|
||||
vi.mocked(api.saveManualFetch).mockResolvedValue({
|
||||
id: 'plf-1',
|
||||
personalDataUuid: 'pd-2',
|
||||
workspaceId: 'ws-1',
|
||||
childId: 'c-1',
|
||||
source: 'manual',
|
||||
fetchedAt: '2026-01-01',
|
||||
totalDays: 240,
|
||||
daysUsed: 0,
|
||||
daysLeft: 240,
|
||||
reservedDaysTotal: 45,
|
||||
reservedDaysLeft: 45,
|
||||
transferableDaysLeft: 0,
|
||||
dubbeldagarLeft: 60,
|
||||
tierData: {
|
||||
sjukpenningTotal: 195,
|
||||
sjukpenningUsed: 0,
|
||||
sjukpenningLeft: 195,
|
||||
lagstaNivaTotal: 45,
|
||||
lagstaNivaUsed: 0,
|
||||
lagstaNivaLeft: 45,
|
||||
},
|
||||
createdAt: '2026-01-01',
|
||||
updatedAt: '2026-01-01',
|
||||
})
|
||||
renderSettings()
|
||||
|
||||
fireEvent.click(screen.getByTestId('settings-balance-pd-2'))
|
||||
expect(await screen.findByText('Sams dagar kvar')).toBeInTheDocument()
|
||||
// The vacation-days table above also has per-year "Spara" buttons —
|
||||
// the balance form's is the last one rendered (it's mounted at the
|
||||
// very end of the page).
|
||||
const saveButtons = screen.getAllByRole('button', { name: 'Spara' })
|
||||
fireEvent.click(saveButtons[saveButtons.length - 1]!)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(api.saveManualFetch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ personalDataUuid: 'pd-2', workspaceId: 'ws-1', childId: 'c-1', source: 'manual' })
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('opens the balance form for the logged-in user\'s own row too', async () => {
|
||||
renderSettings()
|
||||
|
||||
fireEvent.click(screen.getByTestId('settings-balance-pd-1'))
|
||||
expect(await screen.findByText('Dina dagar kvar')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('toggles week numbers', () => {
|
||||
|
||||
Reference in New Issue
Block a user