Spaces:
Sleeping
Sleeping
| 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') }) | |
| }) | |
| }) | |