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() }) }) })