Build plan-shape test data with the typed factories
PlanResults, Plan and usePlanProposal still hand-rolled proposal objects inline -- roughly 29 nested literals in PlanResults alone. Hand-writing a deep shape is the pressure that produced 219 `as any` casts across this suite, so the factories existing but going unused left the trap armed. Values an assertion depends on stay inline as overrides; the factories supply only the scaffolding a reader does not need to see. A type gaining a field now breaks one factory instead of silently passing thirty tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
This commit is contained in:
@@ -2,11 +2,12 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { PlanResults } from './PlanResults'
|
import { PlanResults } from './PlanResults'
|
||||||
import { LanguageProvider } from '@/i18n'
|
import { LanguageProvider } from '@/i18n'
|
||||||
import type { PersonalData, PlanProposal } from '@/types'
|
import type { PersonalData } from '@/types'
|
||||||
|
import { makePersonalData, makePlanProposal, makeWindowOutcome, makePlanWarning, makeSurplusAdvice, makePlanAge4Projection, makeVacationBalance } from '../../../tests/fixtures'
|
||||||
|
|
||||||
const mockMembers: PersonalData[] = [
|
const mockMembers: PersonalData[] = [
|
||||||
{ uuid: 'pd1', workspaceId: 'ws1', name: 'Alex', salaryType: 'monthly', employmentStatus: 'employed', createdAt: '2026-01-01' },
|
makePersonalData({ uuid: 'pd1', name: 'Alex' }),
|
||||||
{ uuid: 'pd2', workspaceId: 'ws1', name: 'Sam', salaryType: 'monthly', employmentStatus: 'employed', createdAt: '2026-01-01' },
|
makePersonalData({ uuid: 'pd2', name: 'Sam' }),
|
||||||
]
|
]
|
||||||
|
|
||||||
// Node's own experimental `localStorage` global shadows jsdom's working
|
// Node's own experimental `localStorage` global shadows jsdom's working
|
||||||
@@ -45,39 +46,6 @@ function renderLocalized(ui: React.ReactElement, locale: 'sv' | 'en') {
|
|||||||
return render(<LanguageProvider>{ui}</LanguageProvider>)
|
return render(<LanguageProvider>{ui}</LanguageProvider>)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildProposal(overrides: Partial<PlanProposal> = {}): PlanProposal {
|
|
||||||
return {
|
|
||||||
days: [],
|
|
||||||
outcomes: [
|
|
||||||
{
|
|
||||||
wishId: 'lw1',
|
|
||||||
wishName: 'Sommar',
|
|
||||||
wishNameMessageKey: null,
|
|
||||||
year: 2026,
|
|
||||||
startDate: '2026-06-15',
|
|
||||||
endDate: '2026-07-26',
|
|
||||||
weeksWished: 6,
|
|
||||||
weeksFunded: 6,
|
|
||||||
shortfallDays: 0,
|
|
||||||
strategy: 'parent_swap',
|
|
||||||
messageKey: 'planOutcomes.windowExplanation',
|
|
||||||
data: { wish_name: 'Sommar', year: 2026, weeks_wished: 6, weeks_funded: 6, shortfall_days: 0 },
|
|
||||||
costBreakdown: { free: 12, vacation: 19, sjukpenning: 11, lagsta: 0 },
|
|
||||||
remedies: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
warnings: [],
|
|
||||||
poolBalances: [
|
|
||||||
{ parentPersonalDataUuid: 'pd1', childId: 'child1', sjukpenningLeft: 87, lagstaLeft: 0, dubbeldagarLeft: 60 },
|
|
||||||
],
|
|
||||||
vacationBalances: [
|
|
||||||
{ parentPersonalDataUuid: 'pd1', year: 2026, totalDays: 25, savedDaysIn: 0, alreadyUsed: 6, isAssumed: false },
|
|
||||||
],
|
|
||||||
surplusAdvice: [],
|
|
||||||
age4Projections: [],
|
|
||||||
...overrides,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('PlanResults', () => {
|
describe('PlanResults', () => {
|
||||||
const originalLanguage = window.navigator.language
|
const originalLanguage = window.navigator.language
|
||||||
@@ -92,45 +60,34 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('shows the "this is a preview" banner', () => {
|
it('shows the "this is a preview" banner', () => {
|
||||||
renderLocalized(<PlanResults proposal={buildProposal()} members={mockMembers} />, 'sv')
|
renderLocalized(<PlanResults proposal={makePlanProposal()} members={mockMembers} />, 'sv')
|
||||||
expect(screen.getByTestId('plan-preview-banner')).toHaveTextContent(/förslag/i)
|
expect(screen.getByTestId('plan-preview-banner')).toHaveTextContent(/förslag/i)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders a multi-year overview row per year with each outcome as a badge', () => {
|
it('renders a multi-year overview row per year with each outcome as a badge', () => {
|
||||||
const proposal = buildProposal({
|
const proposal = makePlanProposal({
|
||||||
outcomes: [
|
outcomes: [
|
||||||
{
|
makeWindowOutcome({
|
||||||
wishId: 'lw1',
|
wishId: 'lw1',
|
||||||
wishName: 'Sommar',
|
wishName: 'Sommar',
|
||||||
wishNameMessageKey: null,
|
|
||||||
year: 2026,
|
year: 2026,
|
||||||
startDate: '2026-06-15',
|
startDate: '2026-06-15',
|
||||||
endDate: '2026-07-26',
|
endDate: '2026-07-26',
|
||||||
weeksWished: 6,
|
weeksWished: 6,
|
||||||
weeksFunded: 6,
|
weeksFunded: 6,
|
||||||
shortfallDays: 0,
|
|
||||||
strategy: 'parent_swap',
|
|
||||||
messageKey: 'planOutcomes.windowExplanation',
|
|
||||||
data: { wish_name: 'Sommar', year: 2026, weeks_wished: 6, weeks_funded: 6, shortfall_days: 0 },
|
|
||||||
costBreakdown: { free: 0, vacation: 0, sjukpenning: 0, lagsta: 0 },
|
costBreakdown: { free: 0, vacation: 0, sjukpenning: 0, lagsta: 0 },
|
||||||
remedies: [],
|
}),
|
||||||
},
|
makeWindowOutcome({
|
||||||
{
|
|
||||||
wishId: 'lw2',
|
wishId: 'lw2',
|
||||||
wishName: 'Jul',
|
wishName: 'Jul',
|
||||||
wishNameMessageKey: null,
|
|
||||||
year: 2027,
|
year: 2027,
|
||||||
startDate: '2027-12-20',
|
startDate: '2027-12-20',
|
||||||
endDate: '2028-01-16',
|
endDate: '2028-01-16',
|
||||||
weeksWished: 4,
|
weeksWished: 4,
|
||||||
weeksFunded: 4,
|
weeksFunded: 4,
|
||||||
shortfallDays: 0,
|
|
||||||
strategy: 'solo',
|
|
||||||
messageKey: 'planOutcomes.windowExplanation',
|
|
||||||
data: { wish_name: 'Jul', year: 2027, weeks_wished: 4, weeks_funded: 4, shortfall_days: 0 },
|
|
||||||
costBreakdown: { free: 0, vacation: 0, sjukpenning: 0, lagsta: 0 },
|
costBreakdown: { free: 0, vacation: 0, sjukpenning: 0, lagsta: 0 },
|
||||||
remedies: [],
|
strategy: 'solo',
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
||||||
@@ -142,7 +99,24 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders a card per window outcome with the day breakdown and explanation', () => {
|
it('renders a card per window outcome with the day breakdown and explanation', () => {
|
||||||
renderLocalized(<PlanResults proposal={buildProposal()} members={mockMembers} />, 'sv')
|
const proposal = makePlanProposal({
|
||||||
|
poolBalances: [{ parentPersonalDataUuid: 'pd1', childId: 'child1', sjukpenningLeft: 87, lagstaLeft: 0, dubbeldagarLeft: 60 }],
|
||||||
|
vacationBalances: [{ parentPersonalDataUuid: 'pd1', year: 2026, totalDays: 25, savedDaysIn: 0, alreadyUsed: 6, isAssumed: false }],
|
||||||
|
outcomes: [
|
||||||
|
makeWindowOutcome({
|
||||||
|
wishId: 'lw1',
|
||||||
|
wishName: 'Sommar',
|
||||||
|
year: 2026,
|
||||||
|
startDate: '2026-06-15',
|
||||||
|
endDate: '2026-07-26',
|
||||||
|
weeksWished: 6,
|
||||||
|
weeksFunded: 6,
|
||||||
|
costBreakdown: { free: 12, vacation: 19, sjukpenning: 11, lagsta: 0 },
|
||||||
|
data: { wish_name: 'Sommar', year: 2026, weeks_wished: 6, weeks_funded: 6, shortfall_days: 0 },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
||||||
|
|
||||||
const card = screen.getByTestId('plan-outcome-lw1-2026')
|
const card = screen.getByTestId('plan-outcome-lw1-2026')
|
||||||
expect(card).toHaveTextContent('Sommar 2026')
|
expect(card).toHaveTextContent('Sommar 2026')
|
||||||
@@ -155,7 +129,22 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders the window explanation in English when the locale is English', () => {
|
it('renders the window explanation in English when the locale is English', () => {
|
||||||
renderLocalized(<PlanResults proposal={buildProposal()} members={mockMembers} />, 'en')
|
const proposal = makePlanProposal({
|
||||||
|
outcomes: [
|
||||||
|
makeWindowOutcome({
|
||||||
|
wishId: 'lw1',
|
||||||
|
wishName: 'Sommar',
|
||||||
|
year: 2026,
|
||||||
|
startDate: '2026-06-15',
|
||||||
|
endDate: '2026-07-26',
|
||||||
|
weeksWished: 6,
|
||||||
|
weeksFunded: 6,
|
||||||
|
costBreakdown: { free: 12, vacation: 19, sjukpenning: 11, lagsta: 0 },
|
||||||
|
data: { wish_name: 'Sommar', year: 2026, weeks_wished: 6, weeks_funded: 6, shortfall_days: 0 },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'en')
|
||||||
|
|
||||||
const card = screen.getByTestId('plan-outcome-lw1-2026')
|
const card = screen.getByTestId('plan-outcome-lw1-2026')
|
||||||
expect(card).toHaveTextContent('Sommar 2026 — 6 weeks wished, 6 weeks funded')
|
expect(card).toHaveTextContent('Sommar 2026 — 6 weeks wished, 6 weeks funded')
|
||||||
@@ -165,12 +154,11 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('shows a shortfall banner with its ranked remedies when a window is underfunded', () => {
|
it('shows a shortfall banner with its ranked remedies when a window is underfunded', () => {
|
||||||
const proposal = buildProposal({
|
const proposal = makePlanProposal({
|
||||||
outcomes: [
|
outcomes: [
|
||||||
{
|
makeWindowOutcome({
|
||||||
wishId: 'lw1',
|
wishId: 'lw1',
|
||||||
wishName: 'Sommar',
|
wishName: 'Sommar',
|
||||||
wishNameMessageKey: null,
|
|
||||||
year: 2026,
|
year: 2026,
|
||||||
startDate: '2026-06-15',
|
startDate: '2026-06-15',
|
||||||
endDate: '2026-07-26',
|
endDate: '2026-07-26',
|
||||||
@@ -178,8 +166,6 @@ describe('PlanResults', () => {
|
|||||||
weeksFunded: 4,
|
weeksFunded: 4,
|
||||||
shortfallDays: 14,
|
shortfallDays: 14,
|
||||||
strategy: 'solo',
|
strategy: 'solo',
|
||||||
messageKey: 'planOutcomes.windowExplanation',
|
|
||||||
data: { wish_name: 'Sommar', year: 2026, weeks_wished: 6, weeks_funded: 4, shortfall_days: 14 },
|
|
||||||
costBreakdown: { free: 12, vacation: 10, sjukpenning: 6, lagsta: 0 },
|
costBreakdown: { free: 12, vacation: 10, sjukpenning: 6, lagsta: 0 },
|
||||||
remedies: [
|
remedies: [
|
||||||
{
|
{
|
||||||
@@ -195,7 +181,7 @@ describe('PlanResults', () => {
|
|||||||
daysFreed: 6,
|
daysFreed: 6,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
||||||
@@ -209,12 +195,11 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders remedies in English when the locale is English', () => {
|
it('renders remedies in English when the locale is English', () => {
|
||||||
const proposal = buildProposal({
|
const proposal = makePlanProposal({
|
||||||
outcomes: [
|
outcomes: [
|
||||||
{
|
makeWindowOutcome({
|
||||||
wishId: 'lw1',
|
wishId: 'lw1',
|
||||||
wishName: 'Sommar',
|
wishName: 'Sommar',
|
||||||
wishNameMessageKey: null,
|
|
||||||
year: 2026,
|
year: 2026,
|
||||||
startDate: '2026-06-15',
|
startDate: '2026-06-15',
|
||||||
endDate: '2026-07-26',
|
endDate: '2026-07-26',
|
||||||
@@ -222,8 +207,6 @@ describe('PlanResults', () => {
|
|||||||
weeksFunded: 4,
|
weeksFunded: 4,
|
||||||
shortfallDays: 14,
|
shortfallDays: 14,
|
||||||
strategy: 'solo',
|
strategy: 'solo',
|
||||||
messageKey: 'planOutcomes.windowExplanation',
|
|
||||||
data: { wish_name: 'Sommar', year: 2026, weeks_wished: 6, weeks_funded: 4, shortfall_days: 14 },
|
|
||||||
costBreakdown: { free: 12, vacation: 10, sjukpenning: 6, lagsta: 0 },
|
costBreakdown: { free: 12, vacation: 10, sjukpenning: 6, lagsta: 0 },
|
||||||
remedies: [
|
remedies: [
|
||||||
{
|
{
|
||||||
@@ -233,7 +216,7 @@ describe('PlanResults', () => {
|
|||||||
daysFreed: 5,
|
daysFreed: 5,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'en')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'en')
|
||||||
@@ -243,17 +226,15 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('displays the days-lost-at-age-4 warning prominently', () => {
|
it('displays the days-lost-at-age-4 warning prominently', () => {
|
||||||
const proposal = buildProposal({
|
const proposal = makePlanProposal({
|
||||||
warnings: [
|
warnings: [
|
||||||
{
|
makePlanWarning({
|
||||||
kind: 'days_lost_at_age_4',
|
|
||||||
messageKey: 'planWarnings.days_lost_at_age_4',
|
|
||||||
data: {
|
data: {
|
||||||
parent_personal_data_uuid: 'pd1',
|
parent_personal_data_uuid: 'pd1',
|
||||||
expiring_days: 117,
|
expiring_days: 117,
|
||||||
fourth_birthday: '2030-03-01',
|
fourth_birthday: '2030-03-01',
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
||||||
@@ -263,17 +244,15 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders the warning body in English, resolving the parent name, when the locale is English', () => {
|
it('renders the warning body in English, resolving the parent name, when the locale is English', () => {
|
||||||
const proposal = buildProposal({
|
const proposal = makePlanProposal({
|
||||||
warnings: [
|
warnings: [
|
||||||
{
|
makePlanWarning({
|
||||||
kind: 'days_lost_at_age_4',
|
|
||||||
messageKey: 'planWarnings.days_lost_at_age_4',
|
|
||||||
data: {
|
data: {
|
||||||
parent_personal_data_uuid: 'pd1',
|
parent_personal_data_uuid: 'pd1',
|
||||||
expiring_days: 117,
|
expiring_days: 117,
|
||||||
fourth_birthday: '2030-03-01',
|
fourth_birthday: '2030-03-01',
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'en')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'en')
|
||||||
@@ -283,29 +262,33 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('resolves parent uuids to names in the balances tables', () => {
|
it('resolves parent uuids to names in the balances tables', () => {
|
||||||
renderLocalized(<PlanResults proposal={buildProposal()} members={mockMembers} />, 'sv')
|
const proposal = makePlanProposal({
|
||||||
|
poolBalances: [{ parentPersonalDataUuid: 'pd1', childId: 'child1', sjukpenningLeft: 87, lagstaLeft: 0, dubbeldagarLeft: 60 }],
|
||||||
|
vacationBalances: [{ parentPersonalDataUuid: 'pd1', year: 2026, totalDays: 25, savedDaysIn: 0, alreadyUsed: 6, isAssumed: false }],
|
||||||
|
})
|
||||||
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
||||||
|
|
||||||
expect(screen.getByTestId('plan-pool-balances')).toHaveTextContent('Alex')
|
expect(screen.getByTestId('plan-pool-balances')).toHaveTextContent('Alex')
|
||||||
expect(screen.getByTestId('plan-vacation-balances')).toHaveTextContent('Alex')
|
expect(screen.getByTestId('plan-vacation-balances')).toHaveTextContent('Alex')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not render the surplus advice or age-4 projection sections when absent', () => {
|
it('does not render the surplus advice or age-4 projection sections when absent', () => {
|
||||||
renderLocalized(<PlanResults proposal={buildProposal()} members={mockMembers} />, 'sv')
|
renderLocalized(<PlanResults proposal={makePlanProposal()} members={mockMembers} />, 'sv')
|
||||||
|
|
||||||
expect(screen.queryByTestId('plan-surplus-advice')).not.toBeInTheDocument()
|
expect(screen.queryByTestId('plan-surplus-advice')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByTestId('plan-age4-projections')).not.toBeInTheDocument()
|
expect(screen.queryByTestId('plan-age4-projections')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders ranked surplus advice when the plan forecasts a surplus (§9.2)', () => {
|
it('renders ranked surplus advice when the plan forecasts a surplus (§9.2)', () => {
|
||||||
const proposal = buildProposal({
|
const proposal = makePlanProposal({
|
||||||
surplusAdvice: [
|
surplusAdvice: [
|
||||||
{
|
makeSurplusAdvice({
|
||||||
kind: 'wish_more_leave',
|
kind: 'wish_more_leave',
|
||||||
messageKey: 'planSurplusAdvice.wish_more_leave',
|
messageKey: 'planSurplusAdvice.wish_more_leave',
|
||||||
daysInvolved: 10,
|
daysInvolved: 10,
|
||||||
data: { parent_personal_data_uuid: 'pd1', child_id: 'child1', weeks: 2 },
|
data: { parent_personal_data_uuid: 'pd1', child_id: 'child1', weeks: 2 },
|
||||||
},
|
}),
|
||||||
{
|
makeSurplusAdvice({
|
||||||
kind: 'use_less_vacation',
|
kind: 'use_less_vacation',
|
||||||
messageKey: 'planSurplusAdvice.use_less_vacation',
|
messageKey: 'planSurplusAdvice.use_less_vacation',
|
||||||
daysInvolved: 10,
|
daysInvolved: 10,
|
||||||
@@ -315,7 +298,7 @@ describe('PlanResults', () => {
|
|||||||
years_text: '2026, 2027',
|
years_text: '2026, 2027',
|
||||||
min_vacation_days_per_year: 20,
|
min_vacation_days_per_year: 20,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
||||||
@@ -328,14 +311,14 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders surplus advice in English, resolving the parent name, when the locale is English', () => {
|
it('renders surplus advice in English, resolving the parent name, when the locale is English', () => {
|
||||||
const proposal = buildProposal({
|
const proposal = makePlanProposal({
|
||||||
surplusAdvice: [
|
surplusAdvice: [
|
||||||
{
|
makeSurplusAdvice({
|
||||||
kind: 'wish_more_leave',
|
kind: 'wish_more_leave',
|
||||||
messageKey: 'planSurplusAdvice.wish_more_leave',
|
messageKey: 'planSurplusAdvice.wish_more_leave',
|
||||||
daysInvolved: 10,
|
daysInvolved: 10,
|
||||||
data: { parent_personal_data_uuid: 'pd1', child_id: 'child1', weeks: 2 },
|
data: { parent_personal_data_uuid: 'pd1', child_id: 'child1', weeks: 2 },
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'en')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'en')
|
||||||
@@ -346,16 +329,16 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders the plan-aware age-4 projection table (§3.3 part 2)', () => {
|
it('renders the plan-aware age-4 projection table (§3.3 part 2)', () => {
|
||||||
const proposal = buildProposal({
|
const proposal = makePlanProposal({
|
||||||
age4Projections: [
|
age4Projections: [
|
||||||
{
|
makePlanAge4Projection({
|
||||||
parentPersonalDataUuid: 'pd1',
|
parentPersonalDataUuid: 'pd1',
|
||||||
childId: 'child1',
|
childId: 'child1',
|
||||||
fourthBirthday: '2030-03-01',
|
fourthBirthday: '2030-03-01',
|
||||||
projectedLeftAtB4: 106,
|
projectedLeftAtB4: 106,
|
||||||
savableAfterB4: 96,
|
savableAfterB4: 96,
|
||||||
expiringAtB4: 10,
|
expiringAtB4: 10,
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
||||||
@@ -374,9 +357,9 @@ describe('PlanResults', () => {
|
|||||||
// never mistaken for showing the same thing (types/entities.ts:
|
// never mistaken for showing the same thing (types/entities.ts:
|
||||||
// PlanAge4Projection vs Age4Projection).
|
// PlanAge4Projection vs Age4Projection).
|
||||||
it('labels the age-4 projection section as previewing this unsaved proposal, in Swedish', () => {
|
it('labels the age-4 projection section as previewing this unsaved proposal, in Swedish', () => {
|
||||||
const proposal = buildProposal({
|
const proposal = makePlanProposal({
|
||||||
age4Projections: [
|
age4Projections: [
|
||||||
{ parentPersonalDataUuid: 'pd1', childId: 'child1', fourthBirthday: '2030-03-01', projectedLeftAtB4: 106, savableAfterB4: 96, expiringAtB4: 10 },
|
makePlanAge4Projection({ parentPersonalDataUuid: 'pd1', childId: 'child1', fourthBirthday: '2030-03-01', projectedLeftAtB4: 106, savableAfterB4: 96, expiringAtB4: 10 }),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
||||||
@@ -388,9 +371,9 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('labels the age-4 projection section as previewing this unsaved proposal, in English', () => {
|
it('labels the age-4 projection section as previewing this unsaved proposal, in English', () => {
|
||||||
const proposal = buildProposal({
|
const proposal = makePlanProposal({
|
||||||
age4Projections: [
|
age4Projections: [
|
||||||
{ parentPersonalDataUuid: 'pd1', childId: 'child1', fourthBirthday: '2030-03-01', projectedLeftAtB4: 106, savableAfterB4: 96, expiringAtB4: 10 },
|
makePlanAge4Projection({ parentPersonalDataUuid: 'pd1', childId: 'child1', fourthBirthday: '2030-03-01', projectedLeftAtB4: 106, savableAfterB4: 96, expiringAtB4: 10 }),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'en')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'en')
|
||||||
@@ -402,9 +385,9 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('shows an assumed badge for a vacation year with no entered data (§3.2)', () => {
|
it('shows an assumed badge for a vacation year with no entered data (§3.2)', () => {
|
||||||
const proposal = buildProposal({
|
const proposal = makePlanProposal({
|
||||||
vacationBalances: [
|
vacationBalances: [
|
||||||
{ parentPersonalDataUuid: 'pd1', year: 2026, totalDays: 25, savedDaysIn: 0, alreadyUsed: 6, isAssumed: true },
|
makeVacationBalance({ parentPersonalDataUuid: 'pd1', year: 2026, totalDays: 25, savedDaysIn: 0, alreadyUsed: 6, isAssumed: true }),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
||||||
@@ -418,11 +401,13 @@ describe('PlanResults', () => {
|
|||||||
// (weeksFunded 0, shortfallDays = the full calendar-day duration, an
|
// (weeksFunded 0, shortfallDays = the full calendar-day duration, an
|
||||||
// all-zero costBreakdown) plus the same windowExplanation message_key — the
|
// all-zero costBreakdown) plus the same windowExplanation message_key — the
|
||||||
// only addition is the 'unplaced' strategy label.
|
// only addition is the 'unplaced' strategy label.
|
||||||
function buildUnplacedOutcome() {
|
|
||||||
return {
|
it('renders an unplaced flexible wish as a 0%-funded shortfall card with the "unplaced" strategy label (sv)', () => {
|
||||||
|
const proposal = makePlanProposal({
|
||||||
|
outcomes: [
|
||||||
|
makeWindowOutcome({
|
||||||
wishId: 'lw3',
|
wishId: 'lw3',
|
||||||
wishName: 'Vår',
|
wishName: 'Vår',
|
||||||
wishNameMessageKey: null,
|
|
||||||
year: 2027,
|
year: 2027,
|
||||||
startDate: '2027-03-01',
|
startDate: '2027-03-01',
|
||||||
endDate: '2027-05-31',
|
endDate: '2027-05-31',
|
||||||
@@ -430,8 +415,6 @@ describe('PlanResults', () => {
|
|||||||
weeksFunded: 0,
|
weeksFunded: 0,
|
||||||
shortfallDays: 42,
|
shortfallDays: 42,
|
||||||
strategy: 'unplaced',
|
strategy: 'unplaced',
|
||||||
messageKey: 'planOutcomes.windowExplanation',
|
|
||||||
data: { wish_name: 'Vår', year: 2027, weeks_wished: 6, weeks_funded: 0, shortfall_days: 42 },
|
|
||||||
costBreakdown: { free: 0, vacation: 0, sjukpenning: 0, lagsta: 0 },
|
costBreakdown: { free: 0, vacation: 0, sjukpenning: 0, lagsta: 0 },
|
||||||
remedies: [
|
remedies: [
|
||||||
{
|
{
|
||||||
@@ -441,11 +424,9 @@ describe('PlanResults', () => {
|
|||||||
daysFreed: 14,
|
daysFreed: 14,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}),
|
||||||
}
|
],
|
||||||
|
})
|
||||||
it('renders an unplaced flexible wish as a 0%-funded shortfall card with the "unplaced" strategy label (sv)', () => {
|
|
||||||
const proposal = buildProposal({ outcomes: [buildUnplacedOutcome()] })
|
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
||||||
|
|
||||||
const card = screen.getByTestId('plan-outcome-lw3-2027')
|
const card = screen.getByTestId('plan-outcome-lw3-2027')
|
||||||
@@ -457,7 +438,30 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders an unplaced flexible wish with the "unplaced" strategy label in English', () => {
|
it('renders an unplaced flexible wish with the "unplaced" strategy label in English', () => {
|
||||||
const proposal = buildProposal({ outcomes: [buildUnplacedOutcome()] })
|
const proposal = makePlanProposal({
|
||||||
|
outcomes: [
|
||||||
|
makeWindowOutcome({
|
||||||
|
wishId: 'lw3',
|
||||||
|
wishName: 'Vår',
|
||||||
|
year: 2027,
|
||||||
|
startDate: '2027-03-01',
|
||||||
|
endDate: '2027-05-31',
|
||||||
|
weeksWished: 6,
|
||||||
|
weeksFunded: 0,
|
||||||
|
shortfallDays: 42,
|
||||||
|
strategy: 'unplaced',
|
||||||
|
costBreakdown: { free: 0, vacation: 0, sjukpenning: 0, lagsta: 0 },
|
||||||
|
remedies: [
|
||||||
|
{
|
||||||
|
kind: 'lower_priority_wish',
|
||||||
|
messageKey: 'planRemedies.lower_priority_wish',
|
||||||
|
data: { wish_name: 'Sommar', from_weeks: 6, to_weeks: 4, freed_days: 14 },
|
||||||
|
daysFreed: 14,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'en')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'en')
|
||||||
|
|
||||||
const card = screen.getByTestId('plan-outcome-lw3-2027')
|
const card = screen.getByTestId('plan-outcome-lw3-2027')
|
||||||
@@ -475,34 +479,18 @@ describe('PlanResults', () => {
|
|||||||
// see src/utils/wishName.ts) lets every surface re-translate it, exactly
|
// see src/utils/wishName.ts) lets every surface re-translate it, exactly
|
||||||
// like WishEditor already does for the wish list itself.
|
// like WishEditor already does for the wish list itself.
|
||||||
describe('wishNameMessageKey (preset wish names follow the UI language)', () => {
|
describe('wishNameMessageKey (preset wish names follow the UI language)', () => {
|
||||||
function buildPresetOutcome() {
|
it('re-translates the name through the badge, card heading, and explanation in Swedish, ignoring the stale stored name', () => {
|
||||||
return {
|
const proposal = makePlanProposal({
|
||||||
|
outcomes: [
|
||||||
|
makeWindowOutcome({
|
||||||
wishId: 'lw1',
|
wishId: 'lw1',
|
||||||
wishName: 'STALE_SWEDISH_TEXT',
|
wishName: 'STALE_SWEDISH_TEXT',
|
||||||
wishNameMessageKey: 'wishEditor.presetSommar',
|
wishNameMessageKey: 'wishEditor.presetSommar',
|
||||||
year: 2026,
|
|
||||||
startDate: '2026-06-15',
|
|
||||||
endDate: '2026-07-26',
|
|
||||||
weeksWished: 6,
|
|
||||||
weeksFunded: 6,
|
|
||||||
shortfallDays: 0,
|
|
||||||
strategy: 'parent_swap',
|
|
||||||
messageKey: 'planOutcomes.windowExplanation',
|
|
||||||
data: {
|
|
||||||
wish_name: 'STALE_SWEDISH_TEXT',
|
|
||||||
wish_name_message_key: 'wishEditor.presetSommar',
|
|
||||||
year: 2026,
|
|
||||||
weeks_wished: 6,
|
|
||||||
weeks_funded: 6,
|
|
||||||
shortfall_days: 0,
|
|
||||||
},
|
|
||||||
costBreakdown: { free: 0, vacation: 0, sjukpenning: 0, lagsta: 0 },
|
costBreakdown: { free: 0, vacation: 0, sjukpenning: 0, lagsta: 0 },
|
||||||
remedies: [],
|
data: { wish_name: 'STALE_SWEDISH_TEXT', wish_name_message_key: 'wishEditor.presetSommar', year: 2026, weeks_wished: 6, weeks_funded: 6, shortfall_days: 0 },
|
||||||
}
|
}),
|
||||||
}
|
],
|
||||||
|
})
|
||||||
it('re-translates the name through the badge, card heading, and explanation in Swedish, ignoring the stale stored name', () => {
|
|
||||||
const proposal = buildProposal({ outcomes: [buildPresetOutcome()] })
|
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
||||||
|
|
||||||
expect(screen.getByText(/Sommar: 6\/6 veckor/)).toBeInTheDocument()
|
expect(screen.getByText(/Sommar: 6\/6 veckor/)).toBeInTheDocument()
|
||||||
@@ -513,7 +501,17 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('re-translates the same name to English when the locale is English', () => {
|
it('re-translates the same name to English when the locale is English', () => {
|
||||||
const proposal = buildProposal({ outcomes: [buildPresetOutcome()] })
|
const proposal = makePlanProposal({
|
||||||
|
outcomes: [
|
||||||
|
makeWindowOutcome({
|
||||||
|
wishId: 'lw1',
|
||||||
|
wishName: 'STALE_SWEDISH_TEXT',
|
||||||
|
wishNameMessageKey: 'wishEditor.presetSommar',
|
||||||
|
costBreakdown: { free: 0, vacation: 0, sjukpenning: 0, lagsta: 0 },
|
||||||
|
data: { wish_name: 'STALE_SWEDISH_TEXT', wish_name_message_key: 'wishEditor.presetSommar', year: 2026, weeks_wished: 6, weeks_funded: 6, shortfall_days: 0 },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'en')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'en')
|
||||||
|
|
||||||
expect(screen.getByText(/Summer: 6\/6 weeks/)).toBeInTheDocument()
|
expect(screen.getByText(/Summer: 6\/6 weeks/)).toBeInTheDocument()
|
||||||
@@ -524,13 +522,21 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('shows the literal name verbatim when wishNameMessageKey is null (free-typed/renamed wish)', () => {
|
it('shows the literal name verbatim when wishNameMessageKey is null (free-typed/renamed wish)', () => {
|
||||||
const outcome = {
|
const proposal = makePlanProposal({
|
||||||
...buildPresetOutcome(),
|
outcomes: [
|
||||||
|
makeWindowOutcome({
|
||||||
|
wishId: 'lw1',
|
||||||
wishName: 'Resa till mormor',
|
wishName: 'Resa till mormor',
|
||||||
wishNameMessageKey: null,
|
wishNameMessageKey: null,
|
||||||
data: { ...buildPresetOutcome().data, wish_name: 'Resa till mormor', wish_name_message_key: null },
|
year: 2026,
|
||||||
}
|
startDate: '2026-06-15',
|
||||||
const proposal = buildProposal({ outcomes: [outcome] })
|
endDate: '2026-07-26',
|
||||||
|
weeksWished: 6,
|
||||||
|
weeksFunded: 6,
|
||||||
|
costBreakdown: { free: 0, vacation: 0, sjukpenning: 0, lagsta: 0 },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
||||||
|
|
||||||
const card = screen.getByTestId('plan-outcome-lw1-2026')
|
const card = screen.getByTestId('plan-outcome-lw1-2026')
|
||||||
@@ -538,17 +544,21 @@ describe('PlanResults', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('falls back to the literal name for a key this build does not recognise', () => {
|
it('falls back to the literal name for a key this build does not recognise', () => {
|
||||||
const outcome = {
|
const proposal = makePlanProposal({
|
||||||
...buildPresetOutcome(),
|
outcomes: [
|
||||||
|
makeWindowOutcome({
|
||||||
|
wishId: 'lw1',
|
||||||
wishName: 'Sommar',
|
wishName: 'Sommar',
|
||||||
wishNameMessageKey: 'wishEditor.thisKeyDoesNotExist',
|
wishNameMessageKey: 'wishEditor.thisKeyDoesNotExist',
|
||||||
data: {
|
year: 2026,
|
||||||
...buildPresetOutcome().data,
|
startDate: '2026-06-15',
|
||||||
wish_name: 'Sommar',
|
endDate: '2026-07-26',
|
||||||
wish_name_message_key: 'wishEditor.thisKeyDoesNotExist',
|
weeksWished: 6,
|
||||||
},
|
weeksFunded: 6,
|
||||||
}
|
costBreakdown: { free: 0, vacation: 0, sjukpenning: 0, lagsta: 0 },
|
||||||
const proposal = buildProposal({ outcomes: [outcome] })
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
||||||
|
|
||||||
const card = screen.getByTestId('plan-outcome-lw1-2026')
|
const card = screen.getByTestId('plan-outcome-lw1-2026')
|
||||||
@@ -556,11 +566,21 @@ describe('PlanResults', () => {
|
|||||||
expect(card).not.toHaveTextContent('wishEditor.thisKeyDoesNotExist')
|
expect(card).not.toHaveTextContent('wishEditor.thisKeyDoesNotExist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('re-translates a lower_priority_wish remedy’s referenced sibling name independently of the outcome’s own wish', () => {
|
it('re-translates a lower_priority_wish remedy\'s referenced sibling name independently of the outcome\'s own wish', () => {
|
||||||
const outcome = {
|
const proposal = makePlanProposal({
|
||||||
...buildPresetOutcome(),
|
outcomes: [
|
||||||
|
makeWindowOutcome({
|
||||||
|
wishId: 'lw1',
|
||||||
|
wishName: 'STALE_SWEDISH_TEXT',
|
||||||
|
wishNameMessageKey: 'wishEditor.presetSommar',
|
||||||
|
year: 2026,
|
||||||
|
startDate: '2026-06-15',
|
||||||
|
endDate: '2026-07-26',
|
||||||
|
weeksWished: 6,
|
||||||
weeksFunded: 4,
|
weeksFunded: 4,
|
||||||
shortfallDays: 14,
|
shortfallDays: 14,
|
||||||
|
strategy: 'parent_swap',
|
||||||
|
costBreakdown: { free: 0, vacation: 0, sjukpenning: 0, lagsta: 0 },
|
||||||
remedies: [
|
remedies: [
|
||||||
{
|
{
|
||||||
kind: 'lower_priority_wish',
|
kind: 'lower_priority_wish',
|
||||||
@@ -575,8 +595,9 @@ describe('PlanResults', () => {
|
|||||||
daysFreed: 6,
|
daysFreed: 6,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}),
|
||||||
const proposal = buildProposal({ outcomes: [outcome] })
|
],
|
||||||
|
})
|
||||||
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
renderLocalized(<PlanResults proposal={proposal} members={mockMembers} />, 'sv')
|
||||||
|
|
||||||
const remedies = screen.getByTestId('plan-remedies-lw1-2026')
|
const remedies = screen.getByTestId('plan-remedies-lw1-2026')
|
||||||
|
|||||||
@@ -2,18 +2,11 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|||||||
import { renderHook, act } from '@testing-library/react'
|
import { renderHook, act } from '@testing-library/react'
|
||||||
import { usePlanProposal } from './usePlanProposal'
|
import { usePlanProposal } from './usePlanProposal'
|
||||||
import * as api from '@/services/api'
|
import * as api from '@/services/api'
|
||||||
|
import { makePlanProposal } from '../../tests/fixtures'
|
||||||
|
|
||||||
vi.mock('@/services/api')
|
vi.mock('@/services/api')
|
||||||
|
|
||||||
const mockProposal = {
|
const mockProposal = makePlanProposal()
|
||||||
days: [],
|
|
||||||
outcomes: [],
|
|
||||||
warnings: [],
|
|
||||||
poolBalances: [],
|
|
||||||
vacationBalances: [],
|
|
||||||
surplusAdvice: [],
|
|
||||||
age4Projections: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('usePlanProposal', () => {
|
describe('usePlanProposal', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ import {
|
|||||||
makeWorkspaceMembersResult,
|
makeWorkspaceMembersResult,
|
||||||
makePlanProposalResult,
|
makePlanProposalResult,
|
||||||
makePlanApplyResult,
|
makePlanApplyResult,
|
||||||
|
makeLeaveWish,
|
||||||
|
makePlanProposal,
|
||||||
|
makeProposedDay,
|
||||||
|
makeWindowOutcome,
|
||||||
|
makePlanWarning,
|
||||||
|
makeDayCollision,
|
||||||
|
makeRejection,
|
||||||
|
makePlanApplyResponse,
|
||||||
} from '../../tests/fixtures'
|
} from '../../tests/fixtures'
|
||||||
|
|
||||||
vi.mock('@/hooks')
|
vi.mock('@/hooks')
|
||||||
@@ -42,52 +50,38 @@ function renderPlanInEnglish() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const mockWish = {
|
const mockWish = makeLeaveWish({
|
||||||
id: 'lw1',
|
id: 'lw1',
|
||||||
workspaceId: 'ws-1',
|
workspaceId: 'ws-1',
|
||||||
childId: null,
|
|
||||||
name: 'Sommar',
|
name: 'Sommar',
|
||||||
nameMessageKey: 'presets.sommar',
|
|
||||||
windowStartMonth: 6,
|
windowStartMonth: 6,
|
||||||
windowStartDay: 15,
|
windowStartDay: 15,
|
||||||
windowEndMonth: 7,
|
windowEndMonth: 7,
|
||||||
windowEndDay: 26,
|
windowEndDay: 26,
|
||||||
targetWeeks: 6,
|
targetWeeks: 6,
|
||||||
recurrence: 'yearly' as const,
|
})
|
||||||
firstYear: 2026,
|
|
||||||
lastYear: null,
|
|
||||||
priority: 1,
|
|
||||||
parentMode: 'either' as const,
|
|
||||||
parentPersonalDataUuid: null,
|
|
||||||
goalOverride: null,
|
|
||||||
windowMode: 'fixed' as const,
|
|
||||||
createdAt: '2026-01-01',
|
|
||||||
updatedAt: '2026-01-01',
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockProposal = {
|
const mockProposal = makePlanProposal({
|
||||||
days: [
|
days: [
|
||||||
{
|
makeProposedDay({
|
||||||
date: '2026-06-15',
|
date: '2026-06-15',
|
||||||
parentPersonalDataUuid: 'pd-1',
|
parentPersonalDataUuid: 'pd-1',
|
||||||
childId: 'c-1',
|
childId: 'c-1',
|
||||||
dayType: 'sjukpenning' as const,
|
dayType: 'sjukpenning',
|
||||||
isDubbeldag: false,
|
|
||||||
wishId: 'lw1',
|
wishId: 'lw1',
|
||||||
reason: 'Sommar',
|
reason: 'Sommar',
|
||||||
},
|
}),
|
||||||
{
|
makeProposedDay({
|
||||||
date: '2026-06-16',
|
date: '2026-06-16',
|
||||||
parentPersonalDataUuid: 'pd-1',
|
parentPersonalDataUuid: 'pd-1',
|
||||||
childId: 'c-1',
|
childId: 'c-1',
|
||||||
dayType: 'vacation' as const,
|
dayType: 'vacation',
|
||||||
isDubbeldag: false,
|
|
||||||
wishId: 'lw1',
|
wishId: 'lw1',
|
||||||
reason: 'Sommar',
|
reason: 'Sommar',
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
outcomes: [
|
outcomes: [
|
||||||
{
|
makeWindowOutcome({
|
||||||
wishId: 'lw1',
|
wishId: 'lw1',
|
||||||
wishName: 'Sommar',
|
wishName: 'Sommar',
|
||||||
wishNameMessageKey: 'presets.sommar',
|
wishNameMessageKey: 'presets.sommar',
|
||||||
@@ -96,26 +90,15 @@ const mockProposal = {
|
|||||||
endDate: '2026-07-26',
|
endDate: '2026-07-26',
|
||||||
weeksWished: 6,
|
weeksWished: 6,
|
||||||
weeksFunded: 6,
|
weeksFunded: 6,
|
||||||
shortfallDays: 0,
|
|
||||||
strategy: 'parent_swap',
|
|
||||||
messageKey: 'planOutcomes.windowExplanation',
|
|
||||||
data: { wish_name: 'Sommar', year: 2026, weeks_wished: 6, weeks_funded: 6, shortfall_days: 0 },
|
|
||||||
costBreakdown: { free: 12, vacation: 19, sjukpenning: 11, lagsta: 0 },
|
costBreakdown: { free: 12, vacation: 19, sjukpenning: 11, lagsta: 0 },
|
||||||
remedies: [],
|
}),
|
||||||
},
|
|
||||||
],
|
],
|
||||||
warnings: [
|
warnings: [
|
||||||
{
|
makePlanWarning({
|
||||||
kind: 'days_lost_at_age_4',
|
|
||||||
messageKey: 'planWarnings.days_lost_at_age_4',
|
|
||||||
data: { parent_personal_data_uuid: 'pd-1', expiring_days: 117, fourth_birthday: '2030-03-01' },
|
data: { parent_personal_data_uuid: 'pd-1', expiring_days: 117, fourth_birthday: '2030-03-01' },
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
poolBalances: [],
|
})
|
||||||
vacationBalances: [],
|
|
||||||
surplusAdvice: [],
|
|
||||||
age4Projections: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Plan', () => {
|
describe('Plan', () => {
|
||||||
let mockAddWish: ReturnType<typeof vi.fn>
|
let mockAddWish: ReturnType<typeof vi.fn>
|
||||||
@@ -130,16 +113,12 @@ describe('Plan', () => {
|
|||||||
mockUpdateWish = vi.fn().mockResolvedValue(mockWish)
|
mockUpdateWish = vi.fn().mockResolvedValue(mockWish)
|
||||||
mockRemoveWish = vi.fn().mockResolvedValue(undefined)
|
mockRemoveWish = vi.fn().mockResolvedValue(undefined)
|
||||||
mockPropose = vi.fn().mockResolvedValue(undefined)
|
mockPropose = vi.fn().mockResolvedValue(undefined)
|
||||||
mockApply = vi.fn().mockResolvedValue({
|
mockApply = vi.fn().mockResolvedValue(
|
||||||
|
makePlanApplyResponse({
|
||||||
accepted: true,
|
accepted: true,
|
||||||
createdCount: 2,
|
createdCount: 2,
|
||||||
skippedIdenticalCount: 0,
|
|
||||||
skippedFreeCount: 0,
|
|
||||||
replacedCount: 0,
|
|
||||||
collisions: [],
|
|
||||||
rejections: [],
|
|
||||||
createdDays: [],
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
vi.mocked(useWorkspace).mockReturnValue(
|
vi.mocked(useWorkspace).mockReturnValue(
|
||||||
makeWorkspaceContextValue({
|
makeWorkspaceContextValue({
|
||||||
@@ -424,24 +403,20 @@ describe('Plan', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
mockApply.mockResolvedValue({
|
mockApply.mockResolvedValue(
|
||||||
|
makePlanApplyResponse({
|
||||||
accepted: false,
|
accepted: false,
|
||||||
createdCount: 0,
|
|
||||||
skippedIdenticalCount: 0,
|
|
||||||
skippedFreeCount: 0,
|
|
||||||
replacedCount: 0,
|
|
||||||
collisions: [
|
collisions: [
|
||||||
{
|
makeDayCollision({
|
||||||
date: '2026-06-15',
|
date: '2026-06-15',
|
||||||
parentPersonalDataUuid: 'pd-1',
|
parentPersonalDataUuid: 'pd-1',
|
||||||
childId: 'c-1',
|
childId: 'c-1',
|
||||||
existingDayType: 'vacation',
|
existingDayType: 'vacation',
|
||||||
proposedDayType: 'sjukpenning',
|
proposedDayType: 'sjukpenning',
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
rejections: [],
|
|
||||||
createdDays: [],
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
renderPlan()
|
renderPlan()
|
||||||
|
|
||||||
@@ -481,16 +456,13 @@ describe('Plan', () => {
|
|||||||
rejections: [],
|
rejections: [],
|
||||||
createdDays: [],
|
createdDays: [],
|
||||||
})
|
})
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce(
|
||||||
|
makePlanApplyResponse({
|
||||||
accepted: true,
|
accepted: true,
|
||||||
createdCount: 2,
|
createdCount: 2,
|
||||||
skippedIdenticalCount: 0,
|
|
||||||
skippedFreeCount: 0,
|
|
||||||
replacedCount: 1,
|
replacedCount: 1,
|
||||||
collisions: [],
|
|
||||||
rejections: [],
|
|
||||||
createdDays: [],
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
renderPlan()
|
renderPlan()
|
||||||
|
|
||||||
@@ -517,24 +489,20 @@ describe('Plan', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
mockApply.mockResolvedValue({
|
mockApply.mockResolvedValue(
|
||||||
|
makePlanApplyResponse({
|
||||||
accepted: false,
|
accepted: false,
|
||||||
createdCount: 0,
|
|
||||||
skippedIdenticalCount: 0,
|
|
||||||
skippedFreeCount: 0,
|
|
||||||
replacedCount: 0,
|
|
||||||
collisions: [
|
collisions: [
|
||||||
{
|
makeDayCollision({
|
||||||
date: '2026-06-15',
|
date: '2026-06-15',
|
||||||
parentPersonalDataUuid: 'pd-1',
|
parentPersonalDataUuid: 'pd-1',
|
||||||
childId: 'c-1',
|
childId: 'c-1',
|
||||||
existingDayType: 'vacation',
|
existingDayType: 'vacation',
|
||||||
proposedDayType: 'sjukpenning',
|
proposedDayType: 'sjukpenning',
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
rejections: [],
|
|
||||||
createdDays: [],
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
renderPlan()
|
renderPlan()
|
||||||
|
|
||||||
@@ -558,21 +526,18 @@ describe('Plan', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
mockApply.mockResolvedValue({
|
mockApply.mockResolvedValue(
|
||||||
|
makePlanApplyResponse({
|
||||||
accepted: true,
|
accepted: true,
|
||||||
createdCount: 1,
|
createdCount: 1,
|
||||||
skippedIdenticalCount: 0,
|
|
||||||
skippedFreeCount: 0,
|
|
||||||
replacedCount: 0,
|
|
||||||
collisions: [],
|
|
||||||
rejections: [
|
rejections: [
|
||||||
{
|
makeRejection({
|
||||||
messageKey: 'planApply.rejection.insufficientBalance',
|
messageKey: 'planApply.rejection.insufficientBalance',
|
||||||
data: { tier: 'sjukpenning', needed: 2, remaining: 1 },
|
data: { tier: 'sjukpenning', needed: 2, remaining: 1 },
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
createdDays: [],
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
renderPlan()
|
renderPlan()
|
||||||
|
|
||||||
@@ -593,21 +558,18 @@ describe('Plan', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
mockApply.mockResolvedValue({
|
mockApply.mockResolvedValue(
|
||||||
|
makePlanApplyResponse({
|
||||||
accepted: true,
|
accepted: true,
|
||||||
createdCount: 1,
|
createdCount: 1,
|
||||||
skippedIdenticalCount: 0,
|
|
||||||
skippedFreeCount: 0,
|
|
||||||
replacedCount: 0,
|
|
||||||
collisions: [],
|
|
||||||
rejections: [
|
rejections: [
|
||||||
{
|
makeRejection({
|
||||||
messageKey: 'planApply.rejection.insufficientBalance',
|
messageKey: 'planApply.rejection.insufficientBalance',
|
||||||
data: { tier: 'sjukpenning', needed: 2, remaining: 1 },
|
data: { tier: 'sjukpenning', needed: 2, remaining: 1 },
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
createdDays: [],
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
vi.stubGlobal('localStorage', {
|
vi.stubGlobal('localStorage', {
|
||||||
getItem: () => null,
|
getItem: () => null,
|
||||||
@@ -654,21 +616,21 @@ describe('Plan', () => {
|
|||||||
it('clears success message when user proposes again after successful apply', async () => {
|
it('clears success message when user proposes again after successful apply', async () => {
|
||||||
// Create two different proposals
|
// Create two different proposals
|
||||||
const mockProposal1 = mockProposal
|
const mockProposal1 = mockProposal
|
||||||
const mockProposal2 = {
|
const mockProposal2 = makePlanProposal({
|
||||||
...mockProposal,
|
|
||||||
days: [
|
days: [
|
||||||
...mockProposal.days,
|
...mockProposal.days,
|
||||||
{
|
makeProposedDay({
|
||||||
date: '2026-07-01',
|
date: '2026-07-01',
|
||||||
parentPersonalDataUuid: 'pd-1',
|
parentPersonalDataUuid: 'pd-1',
|
||||||
childId: 'c-1',
|
childId: 'c-1',
|
||||||
dayType: 'sjukpenning' as const,
|
dayType: 'sjukpenning',
|
||||||
isDubbeldag: false,
|
|
||||||
wishId: 'lw1',
|
wishId: 'lw1',
|
||||||
reason: 'Sommar',
|
reason: 'Sommar',
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
}
|
outcomes: mockProposal.outcomes,
|
||||||
|
warnings: mockProposal.warnings,
|
||||||
|
})
|
||||||
|
|
||||||
// Track proposal state changes
|
// Track proposal state changes
|
||||||
let proposalData = mockProposal1
|
let proposalData = mockProposal1
|
||||||
@@ -743,24 +705,20 @@ describe('Plan', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Mock apply to return a collision on first call
|
// Mock apply to return a collision on first call
|
||||||
mockApply.mockResolvedValue({
|
mockApply.mockResolvedValue(
|
||||||
|
makePlanApplyResponse({
|
||||||
accepted: false,
|
accepted: false,
|
||||||
createdCount: 0,
|
|
||||||
skippedIdenticalCount: 0,
|
|
||||||
skippedFreeCount: 0,
|
|
||||||
replacedCount: 0,
|
|
||||||
collisions: [
|
collisions: [
|
||||||
{
|
makeDayCollision({
|
||||||
date: '2026-06-15',
|
date: '2026-06-15',
|
||||||
parentPersonalDataUuid: 'pd-1',
|
parentPersonalDataUuid: 'pd-1',
|
||||||
childId: 'c-1',
|
childId: 'c-1',
|
||||||
existingDayType: 'vacation',
|
existingDayType: 'vacation',
|
||||||
proposedDayType: 'sjukpenning',
|
proposedDayType: 'sjukpenning',
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
rejections: [],
|
|
||||||
createdDays: [],
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const { rerender } = renderPlan()
|
const { rerender } = renderPlan()
|
||||||
|
|
||||||
@@ -797,16 +755,12 @@ describe('Plan', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
mockApply.mockResolvedValue({
|
mockApply.mockResolvedValue(
|
||||||
|
makePlanApplyResponse({
|
||||||
accepted: true,
|
accepted: true,
|
||||||
createdCount: 5,
|
createdCount: 5,
|
||||||
skippedIdenticalCount: 0,
|
|
||||||
skippedFreeCount: 0,
|
|
||||||
replacedCount: 0,
|
|
||||||
collisions: [],
|
|
||||||
rejections: [],
|
|
||||||
createdDays: [],
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
renderPlan()
|
renderPlan()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user