Files
F-r-ldraledigplannerare/frontend/src/components/calendar/PeriodPopover.test.tsx
T
jacobh2 c5587c5b46 Ask before deleting without leaving the app
Deleting a period or a day went through the browser's own confirmation box.
It halts the page while it is open, which meant the delete path could not be
driven by anything automated, and it looks nothing like the rest of the app.
Both now ask inline: the first click offers the choice, and only the second
one deletes. That matters more than it did, since deleting a period now takes
its days with it.
2026-09-18 00:49:39 +02:00

310 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(<PeriodPopover {...defaultProps} isOpen={false} />)
expect(container.firstChild).toBeNull()
})
describe('ad-hoc band (no wish)', () => {
it('shows the ad-hoc notice and date range instead of editable fields', () => {
render(<PeriodPopover {...defaultProps} />)
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(<PeriodPopover {...defaultProps} />)
expect(screen.queryByTestId('period-save-button')).not.toBeInTheDocument()
})
it('shows the ad-hoc delete label', () => {
render(<PeriodPopover {...defaultProps} />)
expect(screen.getByTestId('period-delete-button')).toHaveTextContent('Ta bort dessa dagar')
})
it('shows an inline confirmation instead of deleting immediately', () => {
render(<PeriodPopover {...defaultProps} />)
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(<PeriodPopover {...defaultProps} />)
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(<PeriodPopover {...defaultProps} />)
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(<PeriodPopover {...defaultProps} wish={wish} />)
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(<PeriodPopover {...defaultProps} wish={jul} />)
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(<PeriodPopover {...defaultProps} wish={wish} />)
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(<PeriodPopover {...defaultProps} wish={makeLeaveWish({ ...wish, lastYear: 2030 })} />)
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(<PeriodPopover {...defaultProps} wish={wish} />)
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(<PeriodPopover {...defaultProps} wish={makeLeaveWish({ ...wish, lastYear: 2030 })} />)
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(<PeriodPopover {...defaultProps} wish={makeLeaveWish({ ...wish, lastYear: 2030 })} />)
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(<PeriodPopover {...defaultProps} wish={wish} />)
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(<PeriodPopover {...defaultProps} wish={wish} />)
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(<PeriodPopover {...defaultProps} wish={makeLeaveWish({ ...wish, lastYear: 2030 })} />)
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(
<PeriodPopover
{...defaultProps}
wish={wish}
parents={[
{ id: 'p1', name: 'Alex' },
{ id: 'p2', name: 'Sam' },
]}
/>,
)
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(
<PeriodPopover
{...defaultProps}
wish={wish}
parents={[
{ id: 'p1', name: 'Alex' },
{ id: 'p2', name: 'Sam' },
]}
/>,
)
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(<PeriodPopover {...defaultProps} wish={wish} />)
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(<PeriodPopover {...defaultProps} wish={wish} />)
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(<PeriodPopover {...defaultProps} wish={wish} />)
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(<PeriodPopover {...defaultProps} wish={wish} />)
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(<PeriodPopover {...defaultProps} wish={wish} />)
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(<PeriodPopover {...defaultProps} wish={wish} error="server said no" />)
expect(screen.getByRole('alert')).toHaveTextContent('server said no')
})
it('shows the loading state while saving', () => {
render(<PeriodPopover {...defaultProps} wish={wish} isSaving />)
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(<PeriodPopover {...defaultProps} wish={presetWish} />)
expect(screen.getByTestId('period-popover-title')).toHaveTextContent('Sommar')
})
})
describe('close behaviour', () => {
it('calls onClose on the close button', () => {
render(<PeriodPopover {...defaultProps} />)
fireEvent.click(screen.getByLabelText('Stäng'))
expect(mockOnClose).toHaveBeenCalled()
})
it('calls onClose on overlay click but not on content click', () => {
render(<PeriodPopover {...defaultProps} />)
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(<PeriodPopover {...defaultProps} />)
fireEvent.click(screen.getByTestId('period-cancel-button'))
expect(mockOnClose).toHaveBeenCalled()
})
})
})