/** * The two authoring gestures the calendar rework is actually about: * dragging a range to create planned days, and deleting them again through * the period popover. Neither was covered end-to-end before — the existing * specs only check rendering (bands, red days, age-4 figures) of state the * API seeded directly. * * A drag no longer creates a `LeaveWish`/synced period on its own — it opens * the day popover pre-selected on every dragged date, and saving writes * exactly those days (ad-hoc `planned_leave_day` rows, no wish). See * `Calendar.tsx`'s pointerup handler and `handleSavePlannedDay`. * * The drag deliberately crosses a month boundary (the last two days of one * month into the first day of the next) — a regression guard for the * leading/trailing-week gating bug, where `CalendarWeek.tsx` used to wire * pointer handlers only for cells matching `weekDates[0]`'s month, so a * trailing day belonging to the next month was inert and a drag could never * reach it. * * Uses a brand-new, empty account (see `support/onboarding.ts`) rather than * the shared demo workspace, so the calendar starts with nothing on it. * * The target month is derived from "today" (never a hardcoded year). */ import { test, expect } from '@playwright/test' import { goToCalendarMonth, calendarDayCell, dragSelectDates } from '../support/calendarNav' import { toIsoDate, addDays } from '../support/dates' import { uniqueEmail, registerAndReachOnboarding, fillChildStep, continueSoloAndSkipBalances } from '../support/onboarding' test.use({ storageState: { cookies: [], origins: [] } }) /** * A month, at least two months out from today, whose last day is NOT a * Sunday — i.e. the grid actually renders a trailing-week row bleeding into * the following month (a month that ends on Sunday has no trailing row at * all after the "no phantom week" fix; see `Calendar.tsx`'s week-building * loop). Returns that month plus the last two of its days and the first day * of the next month — the exact range the drag below crosses. */ function targetRange(): { month: Date; dates: [string, string, string] } { const now = new Date() let month = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 2, 1)) let lastDayOfMonth = new Date(Date.UTC(month.getUTCFullYear(), month.getUTCMonth() + 1, 0)) while (lastDayOfMonth.getUTCDay() === 0 /* Sunday */) { month = new Date(Date.UTC(month.getUTCFullYear(), month.getUTCMonth() + 1, 1)) lastDayOfMonth = new Date(Date.UTC(month.getUTCFullYear(), month.getUTCMonth() + 1, 0)) } const dates = [addDays(lastDayOfMonth, -1), lastDayOfMonth, addDays(lastDayOfMonth, 1)].map(toIsoDate) as [ string, string, string, ] return { month, dates } } test('dragging a range across a month boundary plans every day, and deleting it removes the days for good', async ({ page, }) => { await registerAndReachOnboarding(page, uniqueEmail('drag-delete'), 'Robin Testsson') await fillChildStep(page, 'Nova') await continueSoloAndSkipBalances(page) const { month, dates } = targetRange() const [firstDate, , lastDate] = dates await goToCalendarMonth(page, month) const adhocBand = page.locator('[data-testid^="calendar-band-adhoc:"]') await expect(adhocBand).toHaveCount(0) // a brand-new account — nothing here yet // The drag spans the month's last two days plus the first day of the // following month, rendered as a trailing cell in this same grid. await dragSelectDates(page, dates) // A multi-day drag opens the day popover pre-selected on every dragged // date — it does not create a period/wish by itself. await expect(page.getByTestId('modal-title')).toHaveText('Planera 3 dagar') await page.getByTestId('save-button').click() await expect(page.getByTestId('planned-day-modal__content')).not.toBeVisible() // All three dragged days are one continuous ad-hoc PERIOD, but the grid // renders a multi-week band as one DOM element per week ROW it crosses // (see CalendarWeek.tsx's `computeBandSegments` and // calendar-band.spec.ts) — here Nov 29 (a Sunday, the last column of its // week) and Nov 30 (a Monday, the first column of the next) fall in // different rows even though they're one calendar day apart. Both // elements share the same `data-testid` (derived from the band's key + // overall start date), which IS the "one period" assertion: a real // second period would get its own id. await expect(adhocBand.first()).toBeVisible() await expect .poll(async () => { const testIds = await adhocBand.evaluateAll((els) => els.map((el) => el.getAttribute('data-testid'))) return new Set(testIds).size }) .toBe(1) // Open it through its band — an ad-hoc band (no wishId) shows the plain // date range and can only be deleted, not edited. `.first()` because the // band may render as more than one DOM segment (see above); either one // opens the same underlying period. await adhocBand.first().click() await expect(page.getByTestId('period-popover__content')).toBeVisible() await expect(page.getByTestId('period-adhoc-notice')).toBeVisible() await expect(page.getByTestId('period-popover__content')).toContainText(`${firstDate} – ${lastDate}`) await page.getByTestId('period-delete-button').click() await expect(page.getByTestId('period-delete-confirm')).toBeVisible() // Regression guard for the "Yes, delete" footer bug: the confirm button's // label is long enough to wrap without `white-space: nowrap` on `.btn` // (src/styles/primitives.css), which used to make it visibly taller than // "Cancel" sitting next to it. Assert both the CSS guard and the rendered // heights actually matching. const cancelButton = page.getByTestId('period-delete-cancel-button') const confirmButton = page.getByTestId('period-delete-confirm-button') await expect(cancelButton).toHaveCSS('white-space', 'nowrap') await expect(confirmButton).toHaveCSS('white-space', 'nowrap') const [cancelBox, confirmBox] = await Promise.all([cancelButton.boundingBox(), confirmButton.boundingBox()]) expect(cancelBox).not.toBeNull() expect(confirmBox).not.toBeNull() expect(confirmBox?.height).toBe(cancelBox?.height) await page.getByTestId('period-delete-confirm-button').click() await expect(page.getByTestId('period-popover__overlay')).not.toBeVisible() // The regression this guards ("Remove a period's days along with the // period"): the underlying planned days must actually be gone // server-side, not merely dropped from client state. A reload refetches // from the API, so this is the assertion a surviving-days bug would fail. await page.reload() await goToCalendarMonth(page, month) await expect(page.locator('[data-testid^="calendar-band-adhoc:"]')).toHaveCount(0) // And the day itself is empty again — clicking it opens the "plan a new // day" modal, not an edit of a day that supposedly no longer exists. await calendarDayCell(page, dates[1]).click() await expect(page.getByTestId('modal-title')).toHaveText('Planera en dag') })