import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { PeriodPopover } from './PeriodPopover'
import { makeLeaveWish } from '../../../tests/fixtures'
describe('PeriodPopover', () => {
const mockOnClose = vi.fn()
const mockOnSave = vi.fn()
const mockOnDelete = vi.fn()
const defaultProps = {
isOpen: true,
wish: null,
startDate: '2028-06-15',
endDate: '2028-06-20',
parents: [{ id: 'p1', name: 'Parent 1' }],
onSave: mockOnSave,
onDelete: mockOnDelete,
onClose: mockOnClose,
}
beforeEach(() => {
vi.clearAllMocks()
})
it('returns null when not open', () => {
const { container } = render()
expect(container.firstChild).toBeNull()
})
describe('ad-hoc band (no wish)', () => {
it('shows the ad-hoc notice and date range instead of editable fields', () => {
render()
expect(screen.getByTestId('period-adhoc-notice')).toBeInTheDocument()
expect(screen.getByText('2028-06-15 – 2028-06-20')).toBeInTheDocument()
expect(screen.queryByTestId('period-name-input')).not.toBeInTheDocument()
})
it('does not render a save button', () => {
render()
expect(screen.queryByTestId('period-save-button')).not.toBeInTheDocument()
})
it('shows the ad-hoc delete label', () => {
render()
expect(screen.getByTestId('period-delete-button')).toHaveTextContent('Ta bort dessa dagar')
})
it('shows an inline confirmation instead of deleting immediately', () => {
render()
fireEvent.click(screen.getByTestId('period-delete-button'))
expect(mockOnDelete).not.toHaveBeenCalled()
expect(screen.getByTestId('period-delete-confirm')).toBeInTheDocument()
})
it('calls onDelete after confirming inline', async () => {
mockOnDelete.mockResolvedValue(undefined)
render()
fireEvent.click(screen.getByTestId('period-delete-button'))
fireEvent.click(screen.getByTestId('period-delete-confirm-button'))
await waitFor(() => expect(mockOnDelete).toHaveBeenCalled())
})
it('does not call onDelete when the inline confirmation is cancelled', () => {
render()
fireEvent.click(screen.getByTestId('period-delete-button'))
fireEvent.click(screen.getByTestId('period-delete-cancel-button'))
expect(mockOnDelete).not.toHaveBeenCalled()
expect(screen.queryByTestId('period-delete-confirm')).not.toBeInTheDocument()
expect(screen.getByTestId('period-delete-button')).toBeInTheDocument()
})
})
describe('wish-linked band', () => {
const wish = makeLeaveWish({
id: 'lw-1',
name: 'Sommar',
windowStartMonth: 6,
windowStartDay: 15,
windowEndMonth: 7,
windowEndDay: 26,
firstYear: 2028,
lastYear: null,
recurrence: 'yearly',
parentMode: 'either',
})
it('prefills name/dates/parent/repeat from the wish', () => {
render()
expect(screen.getByTestId('period-name-input')).toHaveValue('Sommar')
expect(screen.getByTestId('period-start-input')).toHaveValue('2028-06-15')
expect(screen.getByTestId('period-end-input')).toHaveValue('2028-07-26')
expect(screen.getByTestId('period-parent-select')).toHaveValue('either')
expect(screen.getByTestId('period-repeat-yearly')).toBeChecked()
})
it('shows the wrapping window correctly for a Jul-style wish (end year = firstYear + 1)', () => {
const jul = makeLeaveWish({
windowStartMonth: 12,
windowStartDay: 20,
windowEndMonth: 1,
windowEndDay: 16,
firstYear: 2028,
})
render()
expect(screen.getByTestId('period-start-input')).toHaveValue('2028-12-20')
expect(screen.getByTestId('period-end-input')).toHaveValue('2029-01-16')
})
it('shows "until age 4" checked when lastYear is null', () => {
render()
expect(screen.getByTestId('period-until-age4')).toBeChecked()
expect(screen.queryByTestId('period-last-year-input')).not.toBeInTheDocument()
})
it('shows an explicit last-year input when lastYear is set', () => {
render()
expect(screen.getByTestId('period-until-age4')).not.toBeChecked()
expect(screen.getByTestId('period-last-year-input')).toHaveValue(2030)
})
it('unchecking "until age 4" reveals a last-year input seeded from the start date\'s year', () => {
render()
expect(screen.getByTestId('period-until-age4')).toBeChecked()
fireEvent.click(screen.getByTestId('period-until-age4'))
expect(screen.getByTestId('period-until-age4')).not.toBeChecked()
expect(screen.getByTestId('period-last-year-input')).toHaveValue(2028)
})
it('re-checking "until age 4" hides the last-year input and clears it on save', async () => {
mockOnSave.mockResolvedValue(undefined)
render()
fireEvent.click(screen.getByTestId('period-until-age4'))
expect(screen.queryByTestId('period-last-year-input')).not.toBeInTheDocument()
fireEvent.click(screen.getByTestId('period-save-button'))
await waitFor(() => expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({ lastYear: null })))
})
it('editing the last-year input updates the value sent on save', async () => {
mockOnSave.mockResolvedValue(undefined)
render()
fireEvent.change(screen.getByTestId('period-last-year-input'), { target: { value: '2032' } })
expect(screen.getByTestId('period-last-year-input')).toHaveValue(2032)
fireEvent.click(screen.getByTestId('period-save-button'))
await waitFor(() => expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({ lastYear: 2032 })))
})
it('toggles to "Never" repeat and hides the yearly options', () => {
render()
fireEvent.click(screen.getByTestId('period-repeat-never'))
expect(screen.queryByTestId('period-yearly-options')).not.toBeInTheDocument()
})
it('calls onSave with the edited name, dates and recurrence', async () => {
mockOnSave.mockResolvedValue(undefined)
render()
fireEvent.change(screen.getByTestId('period-name-input'), { target: { value: 'Sommarledighet' } })
fireEvent.change(screen.getByTestId('period-end-input'), { target: { value: '2028-08-02' } })
fireEvent.click(screen.getByTestId('period-save-button'))
await waitFor(() => {
expect(mockOnSave).toHaveBeenCalledWith({
name: 'Sommarledighet',
startDate: '2028-06-15',
endDate: '2028-08-02',
parentMode: 'either',
parentPersonalDataUuid: null,
recurrence: 'yearly',
lastYear: null,
})
})
})
it('sends lastYear: null when switched to Never even if a last year was set', async () => {
mockOnSave.mockResolvedValue(undefined)
render()
fireEvent.click(screen.getByTestId('period-repeat-never'))
fireEvent.click(screen.getByTestId('period-save-button'))
await waitFor(() => {
expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({ recurrence: 'once', lastYear: null }))
})
})
it('switches parentMode to specific when a member is selected', async () => {
mockOnSave.mockResolvedValue(undefined)
render(
,
)
fireEvent.change(screen.getByTestId('period-parent-select'), { target: { value: 'p2' } })
fireEvent.click(screen.getByTestId('period-save-button'))
await waitFor(() => {
expect(mockOnSave).toHaveBeenCalledWith(
expect.objectContaining({ parentMode: 'specific', parentPersonalDataUuid: 'p2' }),
)
})
})
it('switches parentMode back to "both" after picking a specific member', async () => {
mockOnSave.mockResolvedValue(undefined)
render(
,
)
fireEvent.change(screen.getByTestId('period-parent-select'), { target: { value: 'p2' } })
fireEvent.change(screen.getByTestId('period-parent-select'), { target: { value: 'both' } })
fireEvent.click(screen.getByTestId('period-save-button'))
await waitFor(() => {
expect(mockOnSave).toHaveBeenCalledWith(
expect.objectContaining({ parentMode: 'both', parentPersonalDataUuid: null }),
)
})
})
it('displays a generic local error when onDelete rejects with a non-Error value', async () => {
mockOnDelete.mockRejectedValue('boom')
render()
fireEvent.click(screen.getByTestId('period-delete-button'))
fireEvent.click(screen.getByTestId('period-delete-confirm-button'))
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('Det gick inte att skapa perioden')
})
})
it('calls onDelete for a wish-linked band after confirmation', async () => {
mockOnDelete.mockResolvedValue(undefined)
render()
fireEvent.click(screen.getByTestId('period-delete-button'))
fireEvent.click(screen.getByTestId('period-delete-confirm-button'))
await waitFor(() => expect(mockOnDelete).toHaveBeenCalled())
})
it('shows the wish-linked delete label', () => {
render()
expect(screen.getByTestId('period-delete-button')).toHaveTextContent('Ta bort period')
})
it('displays a local error when onSave rejects', async () => {
mockOnSave.mockRejectedValue(new Error('save exploded'))
render()
fireEvent.click(screen.getByTestId('period-save-button'))
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('save exploded')
})
})
it('displays a generic error when onSave rejects with a non-Error value', async () => {
mockOnSave.mockRejectedValue('boom')
render()
fireEvent.click(screen.getByTestId('period-save-button'))
await waitFor(() => {
expect(screen.getByRole('alert')).not.toHaveTextContent('boom')
expect(screen.getByRole('alert')).toHaveTextContent('Det gick inte att skapa perioden')
})
})
it('displays the error prop directly', () => {
render()
expect(screen.getByRole('alert')).toHaveTextContent('server said no')
})
it('shows the loading state while saving', () => {
render()
expect(screen.getByTestId('period-save-button')).toBeDisabled()
expect(screen.getByTestId('period-save-button')).toHaveTextContent('Sparar…')
})
it('resolves a preset-created title through its message key, not the stale stored name', () => {
const presetWish = makeLeaveWish({ name: 'Stale Cached Name', nameMessageKey: 'wishEditor.seasonSommar' })
render()
expect(screen.getByTestId('period-popover-title')).toHaveTextContent('Sommar')
})
})
describe('close behaviour', () => {
it('calls onClose on the close button', () => {
render()
fireEvent.click(screen.getByLabelText('Stäng'))
expect(mockOnClose).toHaveBeenCalled()
})
it('calls onClose on overlay click but not on content click', () => {
render()
fireEvent.click(screen.getByTestId('period-popover__content'))
expect(mockOnClose).not.toHaveBeenCalled()
fireEvent.click(screen.getByTestId('period-popover__overlay'))
expect(mockOnClose).toHaveBeenCalled()
})
it('calls onClose on the cancel button', () => {
render()
fireEvent.click(screen.getByTestId('period-cancel-button'))
expect(mockOnClose).toHaveBeenCalled()
})
})
})