test(vacay): catch the suites up with the comp/flex column

The kind column changed four things the tests still asserted the old way:
toggleEntry gained a parameter, the toolbar gained the comp modifier, shared
calendar rows carry kind, and a multi-person cell is now segment overlays
instead of one split gradient. splitBackground went with it — nothing but the
test still referenced it.

The toolbar tests picked the half-day button by index, which the new button
shifted; they select by name now so the next modifier does not break them.
This commit is contained in:
Maurice
2026-07-25 11:49:58 +02:00
committed by Maurice
parent c475e5b04c
commit 2672a7a741
6 changed files with 258 additions and 34 deletions
@@ -7,8 +7,8 @@ import { useVacayStore } from '../../store/vacayStore'
import VacayCalendar from './VacayCalendar'
vi.mock('./VacayMonthCard', () => ({
default: ({ month, onCellClick }: any) => (
<div data-testid={`month-card-${month}`}>
default: ({ year, month, onCellClick }: any) => (
<div data-testid={`month-card-${month}`} data-year={year}>
<button onClick={() => onCellClick(`2025-01-${String(month + 1).padStart(2, '0')}`)}>
click-{month}
</button>
@@ -16,6 +16,8 @@ vi.mock('./VacayMonthCard', () => ({
),
}))
const fiscalJuly = { year_type: 'fiscal' as const, year_start_month: 7, year_start_day: 1, hire_date: null }
const basePlan = {
id: 1,
holidays_enabled: false,
@@ -47,6 +49,51 @@ describe('VacayCalendar', () => {
expect(screen.getAllByTestId(/^month-card-/)).toHaveLength(12)
})
it('FE-COMP-VACAYCALENDAR-001a: renders January to December of the selected year by default', () => {
seedStore(useVacayStore, {
selectedYear: 2025,
entries: [],
companyHolidays: [],
holidays: {},
plan: basePlan,
users: [],
selectedUserId: null,
})
render(<VacayCalendar />)
const cards = screen.getAllByTestId(/^month-card-/)
expect(cards[0]).toHaveAttribute('data-testid', 'month-card-0')
expect(cards[0]).toHaveAttribute('data-year', '2025')
expect(cards[11]).toHaveAttribute('data-testid', 'month-card-11')
expect(cards[11]).toHaveAttribute('data-year', '2025')
})
it('FE-COMP-VACAYCALENDAR-001b: rolls the grid over the calendar year for a fiscal window (#737)', () => {
seedStore(useVacayStore, {
selectedYear: 2026,
entries: [],
companyHolidays: [],
holidays: {},
plan: basePlan,
users: [],
selectedUserId: null,
yearSettings: fiscalJuly,
})
render(<VacayCalendar />)
const cards = screen.getAllByTestId(/^month-card-/)
expect(cards).toHaveLength(12)
// Jul 2026 first, Jan 2027 in the middle, Jun 2027 last.
expect(cards[0]).toHaveAttribute('data-testid', 'month-card-6')
expect(cards[0]).toHaveAttribute('data-year', '2026')
expect(cards[6]).toHaveAttribute('data-testid', 'month-card-0')
expect(cards[6]).toHaveAttribute('data-year', '2027')
expect(cards[11]).toHaveAttribute('data-testid', 'month-card-5')
expect(cards[11]).toHaveAttribute('data-year', '2027')
})
it('FE-COMP-VACAYCALENDAR-002: shows vacation mode button by default with username', () => {
seedStore(useVacayStore, {
selectedYear: 2025,
@@ -97,10 +144,10 @@ describe('VacayCalendar', () => {
render(<VacayCalendar />)
// Vacation + half-day buttons stay; only the company button is hidden.
// Vacation + the comp and half-day modifiers stay; only the company button is hidden.
const buttons = screen.getAllByRole('button')
const toolbarButtons = buttons.filter(b => !b.textContent?.startsWith('click-'))
expect(toolbarButtons).toHaveLength(2)
expect(toolbarButtons).toHaveLength(3)
})
it('FE-COMP-VACAYCALENDAR-005: switching to company mode highlights company button', async () => {
@@ -149,7 +196,7 @@ describe('VacayCalendar', () => {
// Click the first month card cell button (month 0 → date '2025-01-01')
await user.click(screen.getByText('click-0'))
expect(toggleEntry).toHaveBeenCalledWith('2025-01-01', 42, 1)
expect(toggleEntry).toHaveBeenCalledWith('2025-01-01', 42, 1, 'vacation')
})
it('FE-COMP-VACAYCALENDAR-006b: half-day mode logs the day as 0.5', async () => {
@@ -169,13 +216,58 @@ describe('VacayCalendar', () => {
render(<VacayCalendar />)
// toolbarButtons[0] = vacation, [1] = half day
const buttons = screen.getAllByRole('button')
const toolbarButtons = buttons.filter(b => !b.textContent?.startsWith('click-'))
await user.click(toolbarButtons[1])
// Comp and half-day are both modifiers in the toolbar — pick by name, not position.
await user.click(screen.getByRole('button', { name: 'Half day' }))
await user.click(screen.getByText('click-0'))
expect(toggleEntry).toHaveBeenCalledWith('2025-01-01', 42, 0.5)
expect(toggleEntry).toHaveBeenCalledWith('2025-01-01', 42, 0.5, 'vacation')
})
it('FE-COMP-VACAYCALENDAR-006c: the comp modifier logs the day as a comp/flex day (#1074)', async () => {
const user = userEvent.setup()
const toggleEntry = vi.fn().mockResolvedValue(undefined)
seedStore(useVacayStore, {
selectedYear: 2025,
entries: [],
companyHolidays: [],
holidays: {},
plan: { ...basePlan, block_weekends: false, company_holidays_enabled: false },
users: [],
selectedUserId: 42,
toggleEntry,
})
render(<VacayCalendar />)
await user.click(screen.getByRole('button', { name: 'Comp / Flex' }))
await user.click(screen.getByText('click-0'))
expect(toggleEntry).toHaveBeenCalledWith('2025-01-01', 42, 1, 'comp')
})
it('FE-COMP-VACAYCALENDAR-006d: comp and half-day stack into a half comp day (#1074)', async () => {
const user = userEvent.setup()
const toggleEntry = vi.fn().mockResolvedValue(undefined)
seedStore(useVacayStore, {
selectedYear: 2025,
entries: [],
companyHolidays: [],
holidays: {},
plan: { ...basePlan, block_weekends: false, company_holidays_enabled: false },
users: [],
selectedUserId: 42,
toggleEntry,
})
render(<VacayCalendar />)
await user.click(screen.getByRole('button', { name: 'Comp / Flex' }))
await user.click(screen.getByRole('button', { name: 'Half day' }))
await user.click(screen.getByText('click-0'))
expect(toggleEntry).toHaveBeenCalledWith('2025-01-01', 42, 0.5, 'comp')
})
it('FE-COMP-VACAYCALENDAR-007: cell click on public holiday toggles vacation entry', async () => {
@@ -198,7 +290,7 @@ describe('VacayCalendar', () => {
// Month 0, button emits '2025-01-01' which is a holiday — should still toggle vacation
await user.click(screen.getByText('click-0'))
expect(toggleEntry).toHaveBeenCalledWith('2025-01-01', undefined, 1)
expect(toggleEntry).toHaveBeenCalledWith('2025-01-01', undefined, 1, 'vacation')
})
it('FE-COMP-VACAYCALENDAR-008: cell click in company mode calls toggleCompanyHoliday', async () => {
@@ -150,18 +150,87 @@ describe('VacayMonthCard', () => {
expect(getFirstWeekLabels()).toEqual(['', '', '', '1', '2', '3', '4'])
})
it('FE-COMP-VACAYMONTHCARD-011: Two vacation entries fill the cell with a split gradient', () => {
it('FE-COMP-VACAYMONTHCARD-011: Two vacation entries split the cell into two clipped halves', () => {
const props = {
...baseProps,
entryMap: {
'2025-01-15': [{ date: '2025-01-15', user_id: 1, person_color: '#6366f1' }, { date: '2025-01-15', user_id: 1, person_color: '#f43f5e' }],
'2025-01-15': [{ date: '2025-01-15', user_id: 1, person_color: '#6366f1' }, { date: '2025-01-15', user_id: 2, person_color: '#f43f5e' }],
},
}
render(<VacayMonthCard {...props} />)
const daySpan = screen.getByText('15')
const cell = daySpan.closest('div') as HTMLElement
// The split gradient is the cell background itself (glass facelift).
expect(cell.style.background).toContain('linear-gradient')
// Each half is its own clipped overlay so it can be solid or hatched on its
// own (#1074) — the diagonal is no longer one gradient on the cell itself.
// jsdom normalises the hex inside color-mix() to rgb().
const halves = [...cell.querySelectorAll<HTMLElement>('div')].filter(d => d.style.clipPath)
expect(halves).toHaveLength(2)
expect(halves[0].style.background).toContain('rgb(99, 102, 241)')
expect(halves[1].style.background).toContain('rgb(244, 63, 94)')
expect(halves.map(h => h.style.background).join()).not.toContain('repeating-linear-gradient')
})
it('FE-COMP-VACAYMONTHCARD-011a: a comp day hatches only its own half of the split (#1074)', () => {
const props = {
...baseProps,
entryMap: {
'2025-01-15': [
{ date: '2025-01-15', user_id: 1, person_color: '#6366f1', kind: 'comp' as const },
{ date: '2025-01-15', user_id: 2, person_color: '#f43f5e' },
],
},
}
render(<VacayMonthCard {...props} />)
const cell = screen.getByText('15').closest('div') as HTMLElement
const halves = [...cell.querySelectorAll<HTMLElement>('div')].filter(d => d.style.clipPath)
expect(halves[0].style.background).toContain('repeating-linear-gradient')
expect(halves[1].style.background).not.toContain('repeating-linear-gradient')
})
it('FE-COMP-VACAYMONTHCARD-011b: a single comp day hatches the whole cell (#1074)', () => {
const props = {
...baseProps,
entryMap: {
'2025-01-15': [{ date: '2025-01-15', user_id: 1, person_color: '#6366f1', kind: 'comp' as const }],
},
}
render(<VacayMonthCard {...props} />)
const cell = screen.getByText('15').closest('div') as HTMLElement
expect(cell.style.background).toContain('repeating-linear-gradient')
})
it('FE-COMP-VACAYMONTHCARD-011c: an all-comp day keeps the white digit and gains a shadow to carry it (#1074)', () => {
const props = {
...baseProps,
entryMap: {
'2025-01-15': [{ date: '2025-01-15', user_id: 1, person_color: '#6366f1', kind: 'comp' as const }],
// A solid vacation fill needs no shadow — the fill itself is the contrast.
'2025-01-16': [{ date: '2025-01-16', user_id: 1, person_color: '#6366f1' }],
},
}
render(<VacayMonthCard {...props} />)
const comp = screen.getByText('15')
const vacation = screen.getByText('16')
expect(comp).toHaveStyle({ color: '#fff' })
// Only that the shadow is there — its exact strength is a design value.
expect(comp.style.textShadow).not.toBe('')
expect(vacation).toHaveStyle({ color: '#fff' })
expect(vacation.style.textShadow).toBe('')
})
it('FE-COMP-VACAYMONTHCARD-011d: a mixed comp/vacation day needs no shadow — a solid half sits under the digit (#1074)', () => {
const props = {
...baseProps,
entryMap: {
'2025-01-15': [
{ date: '2025-01-15', user_id: 1, person_color: '#6366f1', kind: 'comp' as const },
{ date: '2025-01-15', user_id: 2, person_color: '#f43f5e' },
],
},
}
render(<VacayMonthCard {...props} />)
expect(screen.getByText('15')).toHaveStyle({ color: '#fff' })
expect(screen.getByText('15').style.textShadow).toBe('')
})
it('FE-COMP-VACAYMONTHCARD-012: Four vacation entries render quadrant overlay', () => {
@@ -8,6 +8,9 @@ export const FALLBACK_PERSON_COLOR = '#6366f1'
export interface DayVisual {
background: string
numColor: string
// Set on hatched (comp/flex) cells, where the screen shows through between the
// stripes and the digit needs its own contrast (#1074).
textShadow?: string
boxShadow?: string
// At least one person logged this day as a half day (#552) — the cell shows a ½ badge.
half?: boolean
@@ -81,15 +84,6 @@ export function hatchTint(color: string): string {
return `repeating-linear-gradient(45deg, ${color} 0 2.5px, transparent 2.5px 5px)`
}
/** Hard split background when several persons logged the same day. */
export function splitBackground(colors: string[]): string {
const n = colors.length
const stops = colors
.map((c, i) => `${c} ${Math.round((i * 100) / n)}% ${Math.round(((i + 1) * 100) / n)}%`)
.join(',')
return `linear-gradient(105deg,${stops})`
}
/**
* Day-cell color matrix, priority top-down: today ring > company holiday >
* logged persons (pastel, split for several) > public holiday > weekend >
@@ -120,7 +114,13 @@ function baseDayVisual(dateStr: string, dayOfWeek: number, ctx: DayVisualContext
const segments = entries.map(e => ({ color: personTint(e.person_color || FALLBACK_PERSON_COLOR), comp: e.kind === 'comp' }))
const single = segments.length === 1
const background = single ? (segments[0].comp ? hatchTint(segments[0].color) : segments[0].color) : 'transparent'
// The pastel fills are theme-independent surfaces, so every logged day keeps
// the same hard dark digit. An all-comp day is hatched, and the screen shows
// through between its stripes — a light shadow carries the contrast there so
// the digit survives dark mode without changing colour (#1074).
const allComp = segments.every(s => s.comp)
const visual: DayVisual = { background, numColor: '#101013', segments }
if (allComp) visual.textShadow = '0 1px 2px rgba(255,255,255,0.9), 0 0 3px rgba(255,255,255,0.6)'
if (entries.some(e => (e.fraction ?? 1) === 0.5)) visual.half = true
return withSchool(visual)
}
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { dayVisual, monthLead, personTint, splitBackground, type DayVisualContext } from '../../../../src/mobile/screens/vacay/vacayDayModel';
import { dayVisual, hatchTint, monthLead, personTint, type DayVisualContext } from '../../../../src/mobile/screens/vacay/vacayDayModel';
// FE-MOB-VACAY-001 onwards
@@ -44,11 +44,13 @@ describe('vacayDayModel', () => {
// ...but not when company holidays are disabled.
expect(dayVisual('2026-07-15', 3, { ...notToday, companyHolidaysEnabled: false }).numColor).toBe('#101013');
// Logged person: pastel tint of the person color, hard dark digit.
// Logged person: pastel tint of the person color, hard dark digit. One person
// fills the cell directly and still reports the segment (#1074).
const logged = ctx({ todayStr: '2026-01-01', entryMap: { '2026-07-15': [entry] } });
expect(dayVisual('2026-07-15', 3, logged)).toEqual({
background: personTint('#EC4899'),
numColor: '#101013',
segments: [{ color: personTint('#EC4899'), comp: false }],
});
// Public holiday: calendar color fill.
@@ -66,7 +68,7 @@ describe('vacayDayModel', () => {
});
});
it('FE-MOB-VACAY-002: splits logged days evenly and offsets month starts by week start', () => {
it('FE-MOB-VACAY-002: splits logged days into segments and offsets month starts by week start', () => {
const two = ctx({
todayStr: '2026-01-01',
entryMap: {
@@ -76,10 +78,42 @@ describe('vacayDayModel', () => {
],
},
});
expect(dayVisual('2026-07-15', 3, two).background).toBe(
splitBackground([personTint('#EC4899'), personTint('#2FA9A0')]),
);
expect(splitBackground(['a', 'b'])).toBe('linear-gradient(105deg,a 0% 50%,b 50% 100%)');
// Several persons render as segment overlays (#1074), so the cell itself stays
// transparent underneath them and the split lives in `segments`.
const shared = dayVisual('2026-07-15', 3, two);
expect(shared.background).toBe('transparent');
expect(shared.segments).toEqual([
{ color: personTint('#EC4899'), comp: false },
{ color: personTint('#2FA9A0'), comp: false },
]);
// A comp/flex day marks its own segment without touching the other one.
const mixed = ctx({
todayStr: '2026-01-01',
entryMap: {
'2026-07-15': [
{ date: '2026-07-15', user_id: 1, person_color: '#EC4899', kind: 'comp' as const },
{ date: '2026-07-15', user_id: 2, person_color: '#2FA9A0' },
],
},
});
expect(dayVisual('2026-07-15', 3, mixed).segments).toEqual([
{ color: personTint('#EC4899'), comp: true },
{ color: personTint('#2FA9A0'), comp: false },
]);
// A single comp day hatches the whole cell instead.
const soloComp = ctx({
todayStr: '2026-01-01',
entryMap: { '2026-07-15': [{ date: '2026-07-15', user_id: 1, person_color: '#EC4899', kind: 'comp' as const }] },
});
expect(dayVisual('2026-07-15', 3, soloComp).background).toBe(hatchTint(personTint('#EC4899')));
// The hatch lets the screen through, so the digit keeps its colour and gains a
// light shadow to stay readable; a day with a solid segment needs none.
expect(dayVisual('2026-07-15', 3, soloComp).numColor).toBe('#101013');
expect(dayVisual('2026-07-15', 3, soloComp).textShadow).toBeTruthy();
expect(dayVisual('2026-07-15', 3, mixed).numColor).toBe('#101013');
expect(dayVisual('2026-07-15', 3, mixed).textShadow).toBeUndefined();
// July 2026 starts on a Wednesday: 2 leading cells Monday-first, 3 Sunday-first.
expect(monthLead(2026, 6, 1)).toBe(2);
+1 -1
View File
@@ -83,7 +83,7 @@ describe('Vacay e2e (real auth guard + temp SQLite)', () => {
.set('Cookie', sessionCookie(1)).set('X-Socket-Id', 'sock-7').send({ date: '2026-07-01' });
expect(res.status).toBe(200);
expect(res.body).toEqual({ action: 'added' });
expect(svc.toggleEntry).toHaveBeenCalledWith(1, 10, '2026-07-01', undefined, 'sock-7');
expect(svc.toggleEntry).toHaveBeenCalledWith(1, 10, '2026-07-01', undefined, undefined, 'sock-7');
});
it('400 on entries/toggle without a date', async () => {
@@ -137,13 +137,42 @@ describe('VacayController (parity with the legacy /api/addons/vacay route)', ()
it('toggles for the caller', () => {
const toggleEntry = vi.fn().mockReturnValue({ action: 'added' });
expect(makeController({ ...planBase, toggleEntry }).toggleEntry(user, { date: '2026-07-01' }, 'sock')).toEqual({ action: 'added' });
expect(toggleEntry).toHaveBeenCalledWith(1, 10, '2026-07-01', undefined, 'sock');
expect(toggleEntry).toHaveBeenCalledWith(1, 10, '2026-07-01', undefined, undefined, 'sock');
});
it('forwards the half-day fraction (#552)', () => {
const toggleEntry = vi.fn().mockReturnValue({ action: 'added', fraction: 0.5 });
makeController({ ...planBase, toggleEntry }).toggleEntry(user, { date: '2026-07-01', fraction: 0.5 }, 'sock');
expect(toggleEntry).toHaveBeenCalledWith(1, 10, '2026-07-01', 0.5, 'sock');
expect(toggleEntry).toHaveBeenCalledWith(1, 10, '2026-07-01', 0.5, undefined, 'sock');
});
it('forwards the comp/flex leave type (#1074)', () => {
const toggleEntry = vi.fn().mockReturnValue({ action: 'added', kind: 'comp' });
makeController({ ...planBase, toggleEntry }).toggleEntry(user, { date: '2026-07-01', kind: 'comp' }, 'sock');
expect(toggleEntry).toHaveBeenCalledWith(1, 10, '2026-07-01', undefined, 'comp', 'sock');
});
it('GET reads entries over the caller window (#737)', () => {
const getEntries = vi.fn().mockReturnValue({ entries: [], companyHolidays: [] });
makeController({ ...planBase, getEntries }).entries(user, '2026');
expect(getEntries).toHaveBeenCalledWith(10, '2026', 1);
});
});
describe('year settings (#737)', () => {
it('GET wraps the caller settings', () => {
const getYearSettings = vi.fn().mockReturnValue({ year_type: 'calendar' });
expect(makeController({ getYearSettings }).yearSettings(user)).toEqual({ settings: { year_type: 'calendar' } });
expect(getYearSettings).toHaveBeenCalledWith(1);
});
it('PUT saves for the caller and returns the stored settings', () => {
const updateYearSettings = vi.fn().mockReturnValue({ year_type: 'fiscal', year_start_month: 7, year_start_day: 1 });
const body = { year_type: 'fiscal' as const, year_start_month: 7, year_start_day: 1 };
expect(makeController({ updateYearSettings }).updateYearSettings(user, body)).toEqual({
settings: { year_type: 'fiscal', year_start_month: 7, year_start_day: 1 },
});
expect(updateYearSettings).toHaveBeenCalledWith(1, body);
});
});