/** * §3.4 red-day rendering, and the "combined holiday name" trap. * * The backend (`app/utils/holidays.py::get_swedish_holidays`) returns a * COMBINED name like "Påskdagen; Söndag" whenever a fixed public holiday * falls on a Sunday — `get_red_days` keeps that combined string (only a * *bare* "Söndag" is dropped). A careless frontend filter that splits on * ';' and throws away anything containing a weekday name would silently * delete Påskdagen/Pingstdagen's own name along with it. This test asserts * both holidays render as NAMED red days, by their real name, in the * calendar cell's visible text (not hover-only). * * All dates are computed from "today" (see support/dates.ts) — never a * hardcoded year — because Easter/Pentecost's date, and therefore which * year's calendar page to look at, changes every run. */ import { test, expect, type Page } from '@playwright/test' import { addDays, nextEasterOnOrAfter, whitSunday, toIsoDate } from '../support/dates' import { calendarDayCell, goToCalendarMonth } from '../support/calendarNav' /** * Resolves a CSS custom property (declared on :root in styles/global.css) to * the computed `rgb(...)`/`rgba(...)` string the browser would report for an * element actually painted with it — i.e. exactly what `toHaveCSS` compares * against. Reading the token instead of a hardcoded literal means a future * restyle that moves the palette can't silently desync this spec from * calendar.css: whatever `--c-danger`/`--c-text-faint` resolve to at test * time is what's asserted. */ async function resolveCssVar(page: Page, varName: string): Promise { return page.evaluate((name) => { const probe = document.createElement('div') probe.style.color = `var(${name})` document.body.appendChild(probe) const resolved = getComputedStyle(probe).color probe.remove() return resolved }, varName) } /** The day's numeral — the element that carries the red/Saturday colour in * the Almanacka design (cells themselves have no fill). */ function dayNumeral(cell: ReturnType) { return cell.locator('.calendar-day__number') } test.describe('red days, klämdagar and the combined-holiday-name trap', () => { test('Easter Sunday (Påskdagen) renders as a named red day', async ({ page }) => { const easter = nextEasterOnOrAfter(new Date()) await goToCalendarMonth(page, easter) const redInk = await resolveCssVar(page, '--c-danger') // .calendar-day--red-day .calendar-day__number const redFill = await resolveCssVar(page, '--c-danger-weak') const cell = calendarDayCell(page, toIsoDate(easter)) await expect(cell).toHaveClass(/calendar-day--red-day/) // The numeral carries the colour; the cell itself has no red fill. await expect(dayNumeral(cell)).toHaveCSS('color', redInk) await expect(cell).not.toHaveCSS('background-color', redFill) await expect(cell.getByTestId('calendar-day-type-badge')).toHaveText('R') await expect(cell.getByTestId('calendar-day-holiday-name')).toHaveText('Påskdagen') }) test('Whit Sunday (Pingstdagen) renders as a named red day', async ({ page }) => { const whitsun = whitSunday(nextEasterOnOrAfter(new Date())) await goToCalendarMonth(page, whitsun) const redInk = await resolveCssVar(page, '--c-danger') // .calendar-day--red-day .calendar-day__number const redFill = await resolveCssVar(page, '--c-danger-weak') const cell = calendarDayCell(page, toIsoDate(whitsun)) await expect(cell).toHaveClass(/calendar-day--red-day/) await expect(dayNumeral(cell)).toHaveCSS('color', redInk) await expect(cell).not.toHaveCSS('background-color', redFill) await expect(cell.getByTestId('calendar-day-holiday-name')).toHaveText('Pingstdagen') }) test('a plain Sunday is red without a holiday name; a plain Saturday is neutral grey', async ({ page }) => { // Two and three weeks before Easter: far enough from Easter's own // de-facto cluster (Långfredagen/Påskafton/Påskdagen/Annandag påsk, // Easter-2..Easter+1) that neither date is itself a public or de-facto // holiday, just an ordinary weekend. const easter = nextEasterOnOrAfter(new Date()) const plainSunday = addDays(easter, -14) const plainSaturday = addDays(plainSunday, -1) // Navigate by the Sunday's own month, not Easter's: whatever month // `plainSunday` falls in, `plainSaturday` (the day right before it) is // guaranteed to appear in that same month's grid too — either as an // ordinary day in the month, or as a leading fill-in day completing // that first Mon–Sun week row (see Calendar.tsx's `startOffset`) — so // this never depends on the two dates sharing a calendar month. await goToCalendarMonth(page, plainSunday) const redInk = await resolveCssVar(page, '--c-danger') // .calendar-day--red-day .calendar-day__number const redFill = await resolveCssVar(page, '--c-danger-weak') const saturdayInk = await resolveCssVar(page, '--c-text-faint') // .calendar-day--weekend .calendar-day__number const sundayCell = calendarDayCell(page, toIsoDate(plainSunday)) await expect(sundayCell).toHaveClass(/calendar-day--red-day/) await expect(sundayCell).not.toHaveClass(/calendar-day--weekend/) await expect(dayNumeral(sundayCell)).toHaveCSS('color', redInk) await expect(sundayCell).not.toHaveCSS('background-color', redFill) await expect(sundayCell.getByTestId('calendar-day-type-badge')).toHaveText('R') await expect(sundayCell.getByTestId('calendar-day-holiday-name')).toHaveCount(0) const saturdayCell = calendarDayCell(page, toIsoDate(plainSaturday)) await expect(saturdayCell).toHaveClass(/calendar-day--weekend/) await expect(saturdayCell).not.toHaveClass(/calendar-day--red-day/) // A plain Saturday is a faint numeral, not a red one, and no fill. await expect(dayNumeral(saturdayCell)).toHaveCSS('color', saturdayInk) await expect(dayNumeral(saturdayCell)).not.toHaveCSS('color', redInk) await expect(saturdayCell).not.toHaveCSS('background-color', redFill) }) })