Files
F-r-ldraledigplannerare/frontend/src/components/calendar/PeriodPopover.test.tsx
T
jacobh2 dbef997da4 Redraw the calendar as an almanac: gridless cells, typed bands
Day cells lose their borders and fills; red days, klämdagar and
Saturdays are told apart by the numeral's ink with a 12px R/K mark,
today is a filled circle, overrides and exclusions mark the numeral.
Bands carry their funding type by fill, ink and hatching and their
owner by a left cap. The preset rail becomes a chip row, the legend a
help popover, the period popover's three delete buttons fold behind
one "Ta bort…", and solo users pick a type directly in the day modal.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBRLomdLK9jF7wU7a8AA6H
2026-09-19 19:02:02 +02:00

593 lines
27 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()
})
/** The three delete scopes sit behind "Ta bort…" — press it first. */
function openDeleteMenu() {
fireEvent.click(screen.getByTestId('period-delete-menu-button'))
}
it('returns null when not open', () => {
const { container } = render(<PeriodPopover {...defaultProps} isOpen={false} />)
expect(container.firstChild).toBeNull()
})
describe('the delete menu', () => {
it('keeps the three delete scopes hidden until "Ta bort…" is pressed', () => {
render(<PeriodPopover {...defaultProps} />)
expect(screen.queryByTestId('period-delete-group')).not.toBeInTheDocument()
expect(screen.getByTestId('period-delete-menu-button')).toHaveTextContent('Ta bort…')
expect(screen.getByTestId('period-delete-menu-button')).toHaveAttribute('aria-expanded', 'false')
openDeleteMenu()
expect(screen.getByTestId('period-delete-menu-button')).toHaveAttribute('aria-expanded', 'true')
expect(screen.getByTestId('period-delete-group')).toBeInTheDocument()
expect(screen.getByTestId('period-delete-day-button')).toBeInTheDocument()
expect(screen.getByTestId('period-delete-year-button')).toBeInTheDocument()
expect(screen.getByTestId('period-delete-all-years-button')).toBeInTheDocument()
})
it('folds the scopes away again on a second press', () => {
render(<PeriodPopover {...defaultProps} />)
openDeleteMenu()
openDeleteMenu()
expect(screen.queryByTestId('period-delete-group')).not.toBeInTheDocument()
})
it('starts folded again each time the popover reopens', () => {
const { rerender } = render(<PeriodPopover {...defaultProps} />)
openDeleteMenu()
rerender(<PeriodPopover {...defaultProps} isOpen={false} />)
rerender(<PeriodPopover {...defaultProps} isOpen={true} />)
expect(screen.queryByTestId('period-delete-group')).not.toBeInTheDocument()
})
})
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 "this period" label and disables "every year" (nothing recurring to remove)', () => {
render(<PeriodPopover {...defaultProps} />)
openDeleteMenu()
expect(screen.getByTestId('period-delete-year-button')).toHaveTextContent('Ta bort dessa dagar')
expect(screen.getByTestId('period-delete-day-button')).not.toBeDisabled()
expect(screen.getByTestId('period-delete-all-years-button')).toBeDisabled()
expect(screen.getByTestId('period-delete-all-years-button')).toHaveAttribute(
'title',
'Den här perioden är inte kopplad till en återkommande plan.',
)
})
it('shows an inline confirmation instead of deleting immediately', () => {
render(<PeriodPopover {...defaultProps} />)
openDeleteMenu()
fireEvent.click(screen.getByTestId('period-delete-year-button'))
expect(mockOnDelete).not.toHaveBeenCalled()
expect(screen.getByTestId('period-delete-confirm')).toBeInTheDocument()
})
it('calls onDelete with the "year" scope after confirming inline', async () => {
mockOnDelete.mockResolvedValue(undefined)
render(<PeriodPopover {...defaultProps} />)
openDeleteMenu()
fireEvent.click(screen.getByTestId('period-delete-year-button'))
fireEvent.click(screen.getByTestId('period-delete-confirm-button'))
await waitFor(() => expect(mockOnDelete).toHaveBeenCalledWith('year'))
})
it('calls onDelete with the "day" scope for the day button', async () => {
mockOnDelete.mockResolvedValue(undefined)
render(<PeriodPopover {...defaultProps} />)
openDeleteMenu()
fireEvent.click(screen.getByTestId('period-delete-day-button'))
fireEvent.click(screen.getByTestId('period-delete-confirm-button'))
await waitFor(() => expect(mockOnDelete).toHaveBeenCalledWith('day'))
})
it('does not call onDelete when the inline confirmation is cancelled', () => {
render(<PeriodPopover {...defaultProps} />)
openDeleteMenu()
fireEvent.click(screen.getByTestId('period-delete-year-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-year-button')).toBeInTheDocument()
})
})
describe('wish-linked band', () => {
const wish = makeLeaveWish({
id: 'lw-1',
name: 'Sommar',
windowStartMonth: 6,
windowStartDay: 15,
windowEndMonth: 7,
windowEndDay: 26,
firstYear: 2028,
// One year of a repeating wish: the series link is what makes it
// "repeat" now, and what makes the this-year/every-year choice real.
seriesId: 'series-1',
lastYear: null,
parentMode: 'either',
})
it('prefills name/dates/parent/repeat from the wish', () => {
// startDate/endDate here are the clicked band's real placed dates
// (the 2028 occurrence, which happens to equal the wish's first
// occurrence) — see the dedicated "later-year occurrence" tests below
// for the case where they diverge from `wish.firstYear`.
render(
<PeriodPopover {...defaultProps} wish={wish} startDate="2028-06-15" endDate="2028-07-26" />,
)
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('falls back to the wish\'s window-derived dates when no placed dates are supplied', () => {
render(<PeriodPopover {...defaultProps} wish={wish} startDate="" endDate="" />)
expect(screen.getByTestId('period-start-input')).toHaveValue('2028-06-15')
expect(screen.getByTestId('period-end-input')).toHaveValue('2028-07-26')
})
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} startDate="2028-12-20" endDate="2029-01-16" />)
expect(screen.getByTestId('period-start-input')).toHaveValue('2028-12-20')
expect(screen.getByTestId('period-end-input')).toHaveValue('2029-01-16')
})
it('prefills the clicked later-year occurrence\'s dates, not the wish\'s first-occurrence window', () => {
// The wish was first taken in 2026; the user clicked its 2028 band.
const yearlyFrom2026 = makeLeaveWish({
windowStartMonth: 6,
windowStartDay: 15,
windowEndMonth: 7,
windowEndDay: 26,
firstYear: 2026,
seriesId: 'series-1',
})
render(
<PeriodPopover
{...defaultProps}
wish={yearlyFrom2026}
startDate="2028-06-15"
endDate="2028-07-26"
/>,
)
expect(screen.getByTestId('period-start-input')).toHaveValue('2028-06-15')
expect(screen.getByTestId('period-end-input')).toHaveValue('2028-07-26')
// Non-date fields still come from the wish itself, unaffected by which
// occurrence was clicked.
expect(screen.getByTestId('period-repeat-yearly')).toBeChecked()
expect(screen.getByTestId('period-parent-select')).toHaveValue('either')
})
it('prefills a flexible wish\'s actual placed dates, not its bounding window', () => {
// A flexible/wrapping wish whose *window* spans Dec 20 - Jan 16, but
// the allocator actually placed this occurrence a few days later.
const flexible = makeLeaveWish({
windowStartMonth: 12,
windowStartDay: 20,
windowEndMonth: 1,
windowEndDay: 16,
firstYear: 2028,
})
render(
<PeriodPopover {...defaultProps} wish={flexible} startDate="2028-12-27" endDate="2029-01-10" />,
)
expect(screen.getByTestId('period-start-input')).toHaveValue('2028-12-27')
expect(screen.getByTestId('period-end-input')).toHaveValue('2029-01-10')
})
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 () => {
// The end year is series-wide, so the flow starts by widening the scope
// — the control is disabled until then (see the scope tests above).
mockOnSave.mockResolvedValue(undefined)
render(<PeriodPopover {...defaultProps} wish={makeLeaveWish({ ...wish, lastYear: 2030 })} />)
fireEvent.click(screen.getByTestId('period-scope-series'))
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.click(screen.getByTestId('period-scope-series'))
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 })))
})
// Per-year vs whole-series editing. Only a real choice once the
// occurrence belongs to a series — that is what materialising the years
// into their own rows bought (Easter moves, so one year has to be able to
// move without the rest).
describe('this year vs every year', () => {
it('offers the choice for an occurrence of a repeating wish', () => {
render(<PeriodPopover {...defaultProps} wish={wish} />)
expect(screen.getByTestId('period-scope-fieldset')).toBeInTheDocument()
expect(screen.getByTestId('period-scope-occurrence')).toBeChecked()
expect(screen.getByTestId('period-scope-series')).not.toBeChecked()
})
it('does not offer it for a one-off period, where there is nothing to choose between', () => {
render(<PeriodPopover {...defaultProps} wish={makeLeaveWish({ ...wish, seriesId: null })} />)
expect(screen.queryByTestId('period-scope-fieldset')).not.toBeInTheDocument()
})
it('hides it again when the user turns repeating off', () => {
render(<PeriodPopover {...defaultProps} wish={wish} />)
fireEvent.click(screen.getByTestId('period-repeat-never'))
expect(screen.queryByTestId('period-scope-fieldset')).not.toBeInTheDocument()
})
it('saves this year only by default', async () => {
mockOnSave.mockResolvedValue(undefined)
render(<PeriodPopover {...defaultProps} wish={wish} />)
fireEvent.click(screen.getByTestId('period-save-button'))
await waitFor(() =>
expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({ scope: 'occurrence' })),
)
})
it('saves every year once "Alla år" is picked', async () => {
mockOnSave.mockResolvedValue(undefined)
render(<PeriodPopover {...defaultProps} wish={wish} />)
fireEvent.click(screen.getByTestId('period-scope-series'))
fireEvent.click(screen.getByTestId('period-save-button'))
await waitFor(() =>
expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({ scope: 'series' })),
)
})
it('locks the series end year until the edit is series-wide', () => {
// `lastYear` belongs to the whole series, so the backend rejects a
// change to it under the this-year-only scope. Say so in the UI
// rather than let the user hit a 422.
render(<PeriodPopover {...defaultProps} wish={makeLeaveWish({ ...wish, lastYear: 2030 })} />)
expect(screen.getByTestId('period-last-year-input')).toBeDisabled()
expect(screen.getByTestId('period-last-year-locked-hint')).toBeInTheDocument()
fireEvent.click(screen.getByTestId('period-scope-series'))
expect(screen.getByTestId('period-last-year-input')).toBeEnabled()
expect(screen.queryByTestId('period-last-year-locked-hint')).not.toBeInTheDocument()
})
})
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',
// Defaults to the narrow scope: an edit must not silently rewrite
// the other years of the series.
scope: 'occurrence',
lastYear: null,
wantsDubbeldag: false,
})
})
})
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 }),
)
})
})
describe('dubbeldagar checkbox', () => {
it('is enabled only when parentMode is "both"', () => {
render(<PeriodPopover {...defaultProps} wish={makeLeaveWish({ ...wish, parentMode: 'both' })} />)
expect(screen.getByTestId('period-wants-dubbeldag-checkbox')).not.toBeDisabled()
})
it('is disabled for a non-"both" parentMode', () => {
render(<PeriodPopover {...defaultProps} wish={wish} />)
expect(screen.getByTestId('period-wants-dubbeldag-checkbox')).toBeDisabled()
})
it('sends wantsDubbeldag: true only when checked in "both" mode', async () => {
mockOnSave.mockResolvedValue(undefined)
render(<PeriodPopover {...defaultProps} wish={makeLeaveWish({ ...wish, parentMode: 'both' })} />)
fireEvent.click(screen.getByTestId('period-wants-dubbeldag-checkbox'))
fireEvent.click(screen.getByTestId('period-save-button'))
await waitFor(() => expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({ wantsDubbeldag: true })))
})
it('never sends wantsDubbeldag: true outside "both" mode, even if it was checked before switching', async () => {
mockOnSave.mockResolvedValue(undefined)
render(<PeriodPopover {...defaultProps} wish={makeLeaveWish({ ...wish, parentMode: 'both', wantsDubbeldag: true })} />)
fireEvent.change(screen.getByTestId('period-parent-select'), { target: { value: 'either' } })
fireEvent.click(screen.getByTestId('period-save-button'))
await waitFor(() => expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({ wantsDubbeldag: false })))
})
})
// Intent changed (was: '"this day" is disabled for a wish-linked band').
// A single day removed from a planner-placed period is now recorded as a
// `PlannedDayExclusion`, so it no longer reappears on the next sync and
// the option is live — see Calendar.tsx's `excludeDay`.
it('enables "this day" for a wish-linked band and explains what it does', () => {
render(<PeriodPopover {...defaultProps} wish={wish} />)
openDeleteMenu()
expect(screen.getByTestId('period-delete-day-button')).not.toBeDisabled()
expect(screen.getByTestId('period-delete-day-button')).toHaveAttribute(
'title',
'Bara den dagen tas bort. Resten av perioden ligger kvar, och planeraren lägger inte tillbaka dagen.',
)
expect(screen.getByTestId('period-delete-year-button')).not.toBeDisabled()
expect(screen.getByTestId('period-delete-all-years-button')).not.toBeDisabled()
})
it('calls onDelete with the "day" scope for a wish-linked band after confirmation', async () => {
mockOnDelete.mockResolvedValue(undefined)
render(<PeriodPopover {...defaultProps} wish={wish} />)
openDeleteMenu()
fireEvent.click(screen.getByTestId('period-delete-day-button'))
fireEvent.click(screen.getByTestId('period-delete-confirm-button'))
await waitFor(() => expect(mockOnDelete).toHaveBeenCalledWith('day'))
})
it('names the date and the parent in the "this day" confirmation', () => {
render(
<PeriodPopover
{...defaultProps}
wish={wish}
removeDayLabel="17 juni 2028"
removeDayParentName="Alex"
/>,
)
openDeleteMenu()
fireEvent.click(screen.getByTestId('period-delete-day-button'))
expect(screen.getByTestId('period-delete-confirm')).toHaveTextContent(
'Ta bort 17 juni 2028 för Alex?',
)
})
it('falls back to the generic confirmation when the parent is unknown', () => {
render(<PeriodPopover {...defaultProps} wish={wish} removeDayLabel="17 juni 2028" />)
openDeleteMenu()
fireEvent.click(screen.getByTestId('period-delete-day-button'))
expect(screen.getByTestId('period-delete-confirm')).toHaveTextContent(
'Är du säker på att du vill ta bort den här dagen?',
)
})
it('displays a generic local error when onDelete rejects with a non-Error value', async () => {
mockOnDelete.mockRejectedValue('boom')
render(<PeriodPopover {...defaultProps} wish={wish} />)
openDeleteMenu()
fireEvent.click(screen.getByTestId('period-delete-year-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 with the "allYears" scope for a wish-linked band after confirmation', async () => {
mockOnDelete.mockResolvedValue(undefined)
render(<PeriodPopover {...defaultProps} wish={wish} />)
openDeleteMenu()
fireEvent.click(screen.getByTestId('period-delete-all-years-button'))
fireEvent.click(screen.getByTestId('period-delete-confirm-button'))
await waitFor(() => expect(mockOnDelete).toHaveBeenCalledWith('allYears'))
})
it('calls onDelete with the "year" scope for a wish-linked band', async () => {
mockOnDelete.mockResolvedValue(undefined)
render(<PeriodPopover {...defaultProps} wish={wish} />)
openDeleteMenu()
fireEvent.click(screen.getByTestId('period-delete-year-button'))
fireEvent.click(screen.getByTestId('period-delete-confirm-button'))
await waitFor(() => expect(mockOnDelete).toHaveBeenCalledWith('year'))
})
it('shows the wish-linked "this period" and "every year" labels', () => {
render(<PeriodPopover {...defaultProps} wish={wish} />)
openDeleteMenu()
expect(screen.getByTestId('period-delete-year-button')).toHaveTextContent('Den här perioden')
expect(screen.getByTestId('period-delete-all-years-button')).toHaveTextContent('Den här perioden, varje år')
})
it('shows a scope-specific confirmation message for each delete button', () => {
render(<PeriodPopover {...defaultProps} wish={wish} />)
openDeleteMenu()
fireEvent.click(screen.getByTestId('period-delete-year-button'))
expect(screen.getByTestId('period-delete-confirm')).toHaveTextContent(
'Är du säker på att du vill ta bort den här perioden (bara det här året)?',
)
fireEvent.click(screen.getByTestId('period-delete-cancel-button'))
fireEvent.click(screen.getByTestId('period-delete-all-years-button'))
expect(screen.getByTestId('period-delete-confirm')).toHaveTextContent(
'Är du säker på att du vill ta bort perioden — alla år?',
)
})
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} />)
const closeButton = screen.getByLabelText('Stäng')
expect(closeButton.querySelector('[data-icon="close"]')).toBeInTheDocument()
fireEvent.click(closeButton)
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()
})
})
})