mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
ad27c5f6be
- 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)
438 lines
16 KiB
TypeScript
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') })
|
|
})
|
|
})
|