dbef997da4
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
593 lines
27 KiB
TypeScript
593 lines
27 KiB
TypeScript
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()
|
||
})
|
||
})
|
||
})
|