Files
TREK/client/src/components/Vacay/VacaySettings.test.tsx
T
jubnl ad27c5f6be fix: restore broken tests after prerelease workflow refactor
- Export __clearVersionCacheForTests() from adminService; call in
  versionNotification beforeEach to reset module-scoped cache between
  tests (VNOTIF-002..006 failed because VNOTIF-001 cached
  update_available:false, short-circuiting all subsequent test fetches)
- Seed appVersion:'2.9.10' in Navbar test authStore; appVersion moved
  from local useEffect state to authStore in last commit so the test
  render no longer fetches it independently (FE-COMP-NAVBAR-016)
- Add data-testid="weekend-days" to VacaySettings weekend-days
  container; use within() in tests to scope button count to that
  section, fixing false positives from the week-start buttons which
  share the same inline styles (FE-COMP-VACAYSETTINGS-003/004)
- Pass isPrerelease={true} in GitHubPanel FE-ADMIN-GH-007; component
  filters out prerelease releases when isPrerelease=false so the badge
  was never rendered (pre-existing, unrelated to last commit)
2026-04-12 17:19:24 +02:00

438 lines
16 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { render } from '../../../tests/helpers/render'
import { resetAllStores, seedStore } from '../../../tests/helpers/store'
import { server } from '../../../tests/helpers/msw/server'
import { http, HttpResponse } from 'msw'
import { useVacayStore } from '../../store/vacayStore'
import VacaySettings from './VacaySettings'
const basePlan = {
id: 1,
block_weekends: true,
weekend_days: '0,6',
carry_over_enabled: false,
company_holidays_enabled: false,
holidays_enabled: false,
holiday_calendars: [],
}
beforeEach(() => {
resetAllStores()
server.use(
http.get('/api/addons/vacay/holidays/countries', () =>
HttpResponse.json([{ countryCode: 'DE', name: 'Germany' }, { countryCode: 'FR', name: 'France' }])
),
http.get('/api/addons/vacay/holidays/:year/:country', () =>
HttpResponse.json([])
),
)
})
describe('VacaySettings', () => {
it('FE-COMP-VACAYSETTINGS-001: returns null when plan is null', () => {
seedStore(useVacayStore, { plan: null, isFused: false, users: [] })
const { container } = render(<VacaySettings onClose={vi.fn()} />)
expect(container).toBeEmptyDOMElement()
})
it('FE-COMP-VACAYSETTINGS-002: block weekends toggle calls updatePlan', async () => {
const user = userEvent.setup()
const updatePlan = vi.fn().mockResolvedValue(undefined)
seedStore(useVacayStore, {
plan: { ...basePlan, block_weekends: true },
isFused: false,
users: [],
updatePlan,
})
render(<VacaySettings onClose={vi.fn()} />)
// The SettingToggle for block_weekends is the first toggle button
const toggles = screen.getAllByRole('button', { hidden: true })
// Find the toggle button (inline-flex h-6 w-11 button) - there are day buttons + toggle
// The block_weekends toggle is rendered as a button with rounded-full class
// Let's find it by its position - it's the first toggle-style button
const allButtons = screen.getAllByRole('button')
// Day buttons (Mon-Sun) are visible when block_weekends is true, toggle buttons are the ones
// that are NOT day abbreviations. The block_weekends toggle should be before the day buttons.
// Easiest: find the first button that has inline-flex styling (the toggle)
const toggleButton = allButtons.find(b =>
b.className.includes('inline-flex') && b.className.includes('rounded-full')
)
expect(toggleButton).toBeDefined()
await user.click(toggleButton!)
expect(updatePlan).toHaveBeenCalledWith({ block_weekends: false })
})
it('FE-COMP-VACAYSETTINGS-003: weekend day buttons visible when blockWeekends is true', () => {
seedStore(useVacayStore, {
plan: { ...basePlan, block_weekends: true },
isFused: false,
users: [],
})
render(<VacaySettings onClose={vi.fn()} />)
// Day buttons should be visible (Mon, Tue, Wed, Thu, Fri, Sat, Sun)
const dayButtons = within(screen.getByTestId('weekend-days')).getAllByRole('button')
// There should be 7 day buttons
expect(dayButtons.length).toBe(7)
})
it('FE-COMP-VACAYSETTINGS-004: weekend day buttons hidden when blockWeekends is false', () => {
seedStore(useVacayStore, {
plan: { ...basePlan, block_weekends: false },
isFused: false,
users: [],
})
render(<VacaySettings onClose={vi.fn()} />)
// When block_weekends is false, the weekend-days container is not rendered
expect(screen.queryByTestId('weekend-days')).toBeNull()
})
it('FE-COMP-VACAYSETTINGS-005: clicking an active weekend day removes it', async () => {
const user = userEvent.setup()
const updatePlan = vi.fn().mockResolvedValue(undefined)
seedStore(useVacayStore, {
plan: { ...basePlan, block_weekends: true, weekend_days: '0,6' },
isFused: false,
users: [],
updatePlan,
})
render(<VacaySettings onClose={vi.fn()} />)
// Day buttons have inline style with padding: '4px 10px' and borderRadius: 8
const dayButtons = screen.getAllByRole('button').filter(b =>
b.style.padding === '4px 10px'
)
// Order: Mon(1), Tue(2), Wed(3), Thu(4), Fri(5), Sat(6), Sun(0)
// Sun is the last one (index 6), day=0, currently in '0,6'
const sunButton = dayButtons[6]
await user.click(sunButton)
expect(updatePlan).toHaveBeenCalledWith({ weekend_days: '6' })
})
it('FE-COMP-VACAYSETTINGS-006: public holidays section shows add button when enabled', () => {
seedStore(useVacayStore, {
plan: { ...basePlan, holidays_enabled: true, holiday_calendars: [] },
isFused: false,
users: [],
})
render(<VacaySettings onClose={vi.fn()} />)
// The "add calendar" button should be visible
const addButton = screen.getByRole('button', { name: /addCalendar|add calendar|\+/i })
expect(addButton).toBeInTheDocument()
})
it('FE-COMP-VACAYSETTINGS-007: AddCalendarForm appears on add-button click', async () => {
const user = userEvent.setup()
seedStore(useVacayStore, {
plan: { ...basePlan, holidays_enabled: true, holiday_calendars: [] },
isFused: false,
users: [],
})
render(<VacaySettings onClose={vi.fn()} />)
// Find and click the add button (has rounded-md class and is in the holidays section)
const buttons = screen.getAllByRole('button')
const addButton = buttons.find(b => b.className.includes('rounded-md') && b.querySelector('svg'))
expect(addButton).toBeDefined()
await user.click(addButton!)
// After clicking, the AddCalendarForm should be visible with a label input
const inputs = screen.getAllByRole('textbox')
expect(inputs.length).toBeGreaterThan(0)
})
it('FE-COMP-VACAYSETTINGS-008: countries are loaded from API and shown in selector', async () => {
const user = userEvent.setup()
seedStore(useVacayStore, {
plan: { ...basePlan, holidays_enabled: true, holiday_calendars: [] },
isFused: false,
users: [],
})
render(<VacaySettings onClose={vi.fn()} />)
// Click the add button to show AddCalendarForm
const buttons = screen.getAllByRole('button')
const addButton = buttons.find(b => b.className.includes('rounded-md') && b.querySelector('svg'))
await user.click(addButton!)
// Wait for countries to load (the component fetches them on mount)
await waitFor(() => {
// The CustomSelect for country should have Germany and France as options
// CustomSelect renders a button showing the placeholder/selected value
// When opened, options appear. Let's open the dropdown.
const countrySelects = screen.getAllByRole('button').filter(b =>
b.textContent?.includes('selectCountry') ||
b.textContent?.includes('Select') ||
b.textContent?.includes('country')
)
expect(countrySelects.length).toBeGreaterThanOrEqual(1)
})
// Open the country dropdown and check for Germany and France
// Find the country selector button (CustomSelect triggers a dropdown)
const allButtons = screen.getAllByRole('button')
// The country select button in the AddCalendarForm should be one of the later buttons
// Let's look for it by finding the placeholder text
const selectButton = allButtons.find(b =>
b.textContent?.includes('vacay.selectCountry') || b.textContent?.includes('country')
)
if (selectButton) {
await user.click(selectButton)
await waitFor(() => {
expect(screen.queryByText('Germany')).toBeInTheDocument()
})
}
})
it('FE-COMP-VACAYSETTINGS-009: dissolve section shown only when isFused', () => {
seedStore(useVacayStore, {
plan: { ...basePlan },
isFused: true,
users: [],
})
const { rerender } = render(<VacaySettings onClose={vi.fn()} />)
// Dissolve section should be visible
// The dissolve button text comes from t('vacay.dissolveAction')
// In test env with no translations, keys are returned - look for the dissolve button
const buttons = screen.getAllByRole('button')
const dissolveButton = buttons.find(b =>
b.className.includes('bg-red-500') || b.className.includes('bg-red-600')
)
expect(dissolveButton).toBeDefined()
// Re-seed with isFused: false
seedStore(useVacayStore, { isFused: false })
rerender(<VacaySettings onClose={vi.fn()} />)
const buttonsAfter = screen.getAllByRole('button')
const dissolveButtonAfter = buttonsAfter.find(b =>
b.className.includes('bg-red-500') || b.className.includes('bg-red-600')
)
expect(dissolveButtonAfter).toBeUndefined()
})
it('FE-COMP-VACAYSETTINGS-010: dissolve button calls dissolve and onClose', async () => {
const user = userEvent.setup()
const dissolve = vi.fn().mockResolvedValue(undefined)
const onClose = vi.fn()
seedStore(useVacayStore, {
plan: { ...basePlan },
isFused: true,
users: [],
dissolve,
})
render(<VacaySettings onClose={onClose} />)
const buttons = screen.getAllByRole('button')
const dissolveButton = buttons.find(b => b.className.includes('bg-red-500'))
expect(dissolveButton).toBeDefined()
await user.click(dissolveButton!)
await waitFor(() => {
expect(dissolve).toHaveBeenCalled()
expect(onClose).toHaveBeenCalled()
})
})
it('FE-COMP-VACAYSETTINGS-011: calendar row shows delete button and calls deleteHolidayCalendar', async () => {
const user = userEvent.setup()
const deleteHolidayCalendar = vi.fn().mockResolvedValue(undefined)
seedStore(useVacayStore, {
plan: {
...basePlan,
holidays_enabled: true,
holiday_calendars: [{ id: 5, plan_id: 1, region: 'DE', color: '#fecaca', label: null, sort_order: 0 }],
},
isFused: false,
users: [],
deleteHolidayCalendar,
})
render(<VacaySettings onClose={vi.fn()} />)
// The CalendarRow has a Trash2 icon inside a button
const buttons = screen.getAllByRole('button')
// Find the trash button - it has p-1.5 class and shrink-0
const trashButton = buttons.find(b =>
b.className.includes('p-1.5') && b.className.includes('shrink-0')
)
expect(trashButton).toBeDefined()
await user.click(trashButton!)
expect(deleteHolidayCalendar).toHaveBeenCalledWith(5)
})
it('FE-COMP-VACAYSETTINGS-012: calendar row color picker opens on color button click', async () => {
const user = userEvent.setup()
seedStore(useVacayStore, {
plan: {
...basePlan,
holidays_enabled: true,
holiday_calendars: [{ id: 5, plan_id: 1, region: 'DE', color: '#fecaca', label: null, sort_order: 0 }],
},
isFused: false,
users: [],
deleteHolidayCalendar: vi.fn(),
})
render(<VacaySettings onClose={vi.fn()} />)
// The color button in CalendarRow has width:28 and height:28 inline style
const colorButton = screen.getAllByRole('button').find(b =>
b.style.width === '28px' && b.style.height === '28px'
)
expect(colorButton).toBeDefined()
await user.click(colorButton!)
// Color picker should now be visible (12 preset color swatches with width:24)
const swatches = screen.getAllByRole('button').filter(b =>
b.style.width === '24px' && b.style.height === '24px'
)
expect(swatches.length).toBe(12)
})
it('FE-COMP-VACAYSETTINGS-013: clicking a color swatch calls onUpdate with new color', async () => {
const user = userEvent.setup()
const updateHolidayCalendar = vi.fn().mockResolvedValue(undefined)
seedStore(useVacayStore, {
plan: {
...basePlan,
holidays_enabled: true,
holiday_calendars: [{ id: 5, plan_id: 1, region: 'DE', color: '#fecaca', label: null, sort_order: 0 }],
},
isFused: false,
users: [],
updateHolidayCalendar,
})
render(<VacaySettings onClose={vi.fn()} />)
// Open color picker
const colorButton = screen.getAllByRole('button').find(b =>
b.style.width === '28px' && b.style.height === '28px'
)
await user.click(colorButton!)
// Click a different color swatch (second swatch = '#fed7aa', not the current '#fecaca')
const swatches = screen.getAllByRole('button').filter(b =>
b.style.width === '24px' && b.style.height === '24px'
)
await user.click(swatches[1]) // '#fed7aa'
expect(updateHolidayCalendar).toHaveBeenCalledWith(5, { color: '#fed7aa' })
})
it('FE-COMP-VACAYSETTINGS-014: calendar row label blur calls onUpdate when changed', async () => {
const user = userEvent.setup()
const updateHolidayCalendar = vi.fn().mockResolvedValue(undefined)
seedStore(useVacayStore, {
plan: {
...basePlan,
holidays_enabled: true,
holiday_calendars: [{ id: 5, plan_id: 1, region: 'DE', color: '#fecaca', label: null, sort_order: 0 }],
},
isFused: false,
users: [],
updateHolidayCalendar,
})
render(<VacaySettings onClose={vi.fn()} />)
const input = screen.getByRole('textbox')
await user.type(input, 'My Calendar')
await user.tab() // triggers blur
expect(updateHolidayCalendar).toHaveBeenCalledWith(5, { label: 'My Calendar' })
})
it('FE-COMP-VACAYSETTINGS-015: AddCalendarForm cancel button hides form', async () => {
const user = userEvent.setup()
seedStore(useVacayStore, {
plan: { ...basePlan, holidays_enabled: true, holiday_calendars: [] },
isFused: false,
users: [],
})
render(<VacaySettings onClose={vi.fn()} />)
// Open the form
const addButton = screen.getAllByRole('button').find(b =>
b.className.includes('rounded-md') && b.querySelector('svg')
)
await user.click(addButton!)
expect(screen.getAllByRole('textbox').length).toBeGreaterThan(0)
// Click cancel (✕ button)
const cancelButton = screen.getAllByRole('button').find(b => b.textContent === '✕')
expect(cancelButton).toBeDefined()
await user.click(cancelButton!)
// Form should be hidden again - no textbox
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
})
it('FE-COMP-VACAYSETTINGS-016: carry-over toggle calls updatePlan', async () => {
const user = userEvent.setup()
const updatePlan = vi.fn().mockResolvedValue(undefined)
seedStore(useVacayStore, {
plan: { ...basePlan, block_weekends: false, carry_over_enabled: false },
isFused: false,
users: [],
updatePlan,
})
render(<VacaySettings onClose={vi.fn()} />)
const toggleButtons = screen.getAllByRole('button').filter(b =>
b.className.includes('inline-flex') && b.className.includes('rounded-full')
)
// carry_over_enabled is the second toggle (block_weekends, carry_over, company, holidays)
await user.click(toggleButtons[1])
expect(updatePlan).toHaveBeenCalledWith({ carry_over_enabled: true })
})
it('FE-COMP-VACAYSETTINGS-017: company holidays toggle calls updatePlan', async () => {
const user = userEvent.setup()
const updatePlan = vi.fn().mockResolvedValue(undefined)
seedStore(useVacayStore, {
plan: { ...basePlan, block_weekends: false, company_holidays_enabled: false },
isFused: false,
users: [],
updatePlan,
})
render(<VacaySettings onClose={vi.fn()} />)
const toggleButtons = screen.getAllByRole('button').filter(b =>
b.className.includes('inline-flex') && b.className.includes('rounded-full')
)
// company_holidays_enabled is the third toggle
await user.click(toggleButtons[2])
expect(updatePlan).toHaveBeenCalledWith({ company_holidays_enabled: true })
})
it('FE-COMP-VACAYSETTINGS-018: adding weekend day calls updatePlan with day added', async () => {
const user = userEvent.setup()
const updatePlan = vi.fn().mockResolvedValue(undefined)
seedStore(useVacayStore, {
plan: { ...basePlan, block_weekends: true, weekend_days: '6' },
isFused: false,
users: [],
updatePlan,
})
render(<VacaySettings onClose={vi.fn()} />)
// Click Sun button (day=0, currently NOT in '6')
const dayButtons = screen.getAllByRole('button').filter(b =>
b.style.padding === '4px 10px'
)
const sunButton = dayButtons[6] // last button = Sunday
await user.click(sunButton)
expect(updatePlan).toHaveBeenCalledWith({ weekend_days: expect.stringContaining('0') })
})
})