Cover the mappers and labels nothing was watching

The day-override mapper was the only one in the client with no test feeding
it the shape the server actually sends, which is how a feature once shipped
rendering blank cards to everyone while the suite stayed green. The apply
response's collision fields were mapped but only ever exercised with an empty
list, and the leave band's accessible label, composed from translated text and
two dates, was asserted nowhere.

All three were correct; none was guarded. The day-override hook's fixtures now
go through the typed factory rather than hand-written objects.
This commit is contained in:
2026-09-17 22:05:21 +02:00
parent 8e1cf3524c
commit 064bcc2b3a
3 changed files with 133 additions and 10 deletions
@@ -266,6 +266,27 @@ describe('CalendarWeek', () => {
render(<CalendarWeek {...defaultProps} plannedDays={[]} />)
expect(screen.queryByTestId('calendar-event')).not.toBeInTheDocument()
})
it('composes the band aria-label from parent label, type label, and the interpolated date range', () => {
// Regression guard: a tips bug in this repo surfaced as the literal
// un-interpolated "Tip: {title}" in an accessibility label and nothing
// caught it. Assert the real composed string here, not merely that an
// aria-label attribute exists.
const plannedDays = [
pd({ id: '1', date: '2026-01-05', dayType: 'sjukpenning' }),
pd({ id: '2', date: '2026-01-06', dayType: 'sjukpenning' }),
]
render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} parentName="Alice" />)
const band = screen.getAllByTestId(/^calendar-band-/)[0]
expect(band).toHaveAttribute('aria-label', 'Alice · Sjukpenning, 2026-01-05 – 2026-01-06')
})
it('uses the partner label in the band aria-label for the other parent\'s days', () => {
const plannedDays = [pd({ id: '1', date: '2026-01-05', parentPersonalDataUuid: 'other-parent' })]
render(<CalendarWeek {...defaultProps} plannedDays={plannedDays} personalDataUuid="parent-1" />)
const band = screen.getAllByTestId(/^calendar-band-/)[0]
expect(band).toHaveAttribute('aria-label', 'Partner · Semester, 2026-01-05 – 2026-01-05')
})
})
describe('the weekend-gap bug: a wish-linked band bridges free-day gaps', () => {
+4 -10
View File
@@ -8,24 +8,18 @@ vi.mock('@/services/api')
describe('useDayOverrides', () => {
const mockOverrides = [
{
makeDayOverride({
id: 'do-1',
workspaceId: 'ws-1',
date: '2026-12-24',
isWorkingDay: true,
note: 'Jobbade på juldagen',
createdAt: '2026-01-01T00:00:00',
updatedAt: '2026-01-01T00:00:00',
},
{
}),
makeDayOverride({
id: 'do-2',
workspaceId: 'ws-1',
date: '2026-06-20',
isWorkingDay: false,
note: 'Extra ledigt',
createdAt: '2026-01-01T00:00:00',
updatedAt: '2026-01-01T00:00:00',
},
}),
]
beforeEach(() => {
+108
View File
@@ -432,6 +432,74 @@ describe('API response mappers (snake_case → camelCase)', () => {
expect(result.createdAt).toBe('2026-06-14T17:09:47')
})
})
describe('listDayOverridesByWorkspace', () => {
it('maps snake_case day overrides to camelCase', async () => {
// Regression test: toDayOverride was the only mapper in this file with
// no snake_case→camelCase test — this is the exact shape of bug that
// shipped for getTips (see the getTips test below): a missing/wrong
// mapping surfaces only at runtime, never in a suite that mocks the
// module and hand-rolls already-camelCase fixtures.
const rawResponse = [
{
id: 'do-1',
workspace_id: 'ws1',
date: '2026-12-24',
is_working_day: true,
note: 'Jobbade på juldagen',
created_at: '2026-01-01T00:00:00',
updated_at: '2026-01-02T00:00:00',
},
]
fetchMock.mockResolvedValue({
ok: true,
status: 200,
json: async () => rawResponse,
})
const result = await api.listDayOverridesByWorkspace('ws1')
expect(result).toEqual([
{
id: 'do-1',
workspaceId: 'ws1',
date: '2026-12-24',
isWorkingDay: true,
note: 'Jobbade på juldagen',
createdAt: '2026-01-01T00:00:00',
updatedAt: '2026-01-02T00:00:00',
},
])
})
it('maps a null note/created_at/updated_at through unchanged', async () => {
const rawResponse = [
{
id: 'do-2',
workspace_id: 'ws1',
date: '2026-06-20',
is_working_day: false,
note: null,
created_at: null,
updated_at: null,
},
]
fetchMock.mockResolvedValue({
ok: true,
status: 200,
json: async () => rawResponse,
})
const result = await api.listDayOverridesByWorkspace('ws1')
expect(result[0]!.note).toBeNull()
expect(result[0]!.createdAt).toBeNull()
expect(result[0]!.updatedAt).toBeNull()
expect(result[0]!.isWorkingDay).toBe(false)
})
})
})
describe('apiFetch (shared request/response handling)', () => {
@@ -1334,6 +1402,46 @@ describe('endpoint functions (representative coverage)', () => {
])
})
it('applyPlan maps a populated collisions array to camelCase (§3.5)', async () => {
// The only other applyPlan mapping test passes collisions: [], so
// existing_day_type/proposed_day_type were never checked against real
// data — this exercises the actual field renames.
fetchMock.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({
accepted: false,
created_count: 0,
skipped_identical_count: 0,
skipped_free_count: 0,
replaced_count: 0,
collisions: [
{
date: '2026-06-15',
parent_personal_data_uuid: 'pd1',
child_id: 'child1',
existing_day_type: 'vacation',
proposed_day_type: 'sjukpenning',
},
],
rejections: [],
created_days: [],
}),
})
const result = await api.applyPlan('ws1', [], false)
expect(result.collisions).toEqual([
{
date: '2026-06-15',
parentPersonalDataUuid: 'pd1',
childId: 'child1',
existingDayType: 'vacation',
proposedDayType: 'sjukpenning',
},
])
})
it('syncPlan posts the workspace id and maps the nested proposal/applyResult/changes', async () => {
fetchMock.mockResolvedValue({
ok: true,