/** * Unit tests for calendar: dutiesByDate (including edge case end_at < start_at), * calendarEventsByDate. */ import { describe, it, expect, beforeAll, beforeEach } from "vitest"; beforeAll(() => { document.body.innerHTML = '

' + '
' + '
' + ''; }); import { dutiesByDate, calendarEventsByDate, renderCalendar } from "./calendar.js"; import { state } from "./dom.js"; describe("dutiesByDate", () => { it("groups duty by single local day", () => { const duties = [ { full_name: "Alice", start_at: "2025-02-25T09:00:00Z", end_at: "2025-02-25T18:00:00Z", }, ]; const byDate = dutiesByDate(duties); expect(byDate["2025-02-25"]).toHaveLength(1); expect(byDate["2025-02-25"][0].full_name).toBe("Alice"); }); it("spans duty across multiple days", () => { const duties = [ { full_name: "Bob", start_at: "2025-02-25T00:00:00Z", end_at: "2025-02-27T23:59:59Z", }, ]; const byDate = dutiesByDate(duties); const keys = Object.keys(byDate).sort(); expect(keys.length).toBeGreaterThanOrEqual(2); keys.forEach((k) => expect(byDate[k]).toHaveLength(1)); expect(byDate[keys[0]][0].full_name).toBe("Bob"); }); it("skips duty when end_at < start_at (no infinite loop)", () => { const duties = [ { full_name: "Bad", start_at: "2025-02-28T12:00:00Z", end_at: "2025-02-25T08:00:00Z", }, ]; const byDate = dutiesByDate(duties); expect(Object.keys(byDate)).toHaveLength(0); }); it("does not iterate more than MAX_DAYS_PER_DUTY", () => { const start = "2025-01-01T00:00:00Z"; const end = "2026-06-01T00:00:00Z"; const duties = [{ full_name: "Long", start_at: start, end_at: end }]; const byDate = dutiesByDate(duties); const keys = Object.keys(byDate).sort(); expect(keys.length).toBeLessThanOrEqual(367); }); it("handles empty duties", () => { expect(dutiesByDate([])).toEqual({}); }); }); describe("calendarEventsByDate", () => { it("maps events to local date key by UTC date", () => { const events = [ { date: "2025-02-25", summary: "Holiday" }, { date: "2025-02-25", summary: "Meeting" }, { date: "2025-02-26", summary: "Other" }, ]; const byDate = calendarEventsByDate(events); expect(byDate["2025-02-25"]).toEqual(["Holiday", "Meeting"]); expect(byDate["2025-02-26"]).toEqual(["Other"]); }); it("skips events without summary", () => { const events = [{ date: "2025-02-25", summary: null }]; const byDate = calendarEventsByDate(events); expect(byDate["2025-02-25"] || []).toHaveLength(0); }); it("handles null or undefined events", () => { expect(calendarEventsByDate(null)).toEqual({}); expect(calendarEventsByDate(undefined)).toEqual({}); }); }); describe("renderCalendar", () => { beforeEach(() => { state.lang = "en"; }); it("renders 42 cells (6 weeks)", () => { renderCalendar(2025, 0, {}, {}); const calendarEl = document.getElementById("calendar"); const cells = calendarEl?.querySelectorAll(".day") ?? []; expect(cells.length).toBe(42); }); it("sets data-date on each cell to YYYY-MM-DD", () => { renderCalendar(2025, 0, {}, {}); const calendarEl = document.getElementById("calendar"); const cells = Array.from(calendarEl?.querySelectorAll(".day") ?? []); const dates = cells.map((c) => c.getAttribute("data-date")); expect(dates.every((d) => /^\d{4}-\d{2}-\d{2}$/.test(d ?? ""))).toBe(true); }); it("adds today class to cell matching today", () => { const today = new Date(); renderCalendar(today.getFullYear(), today.getMonth(), {}, {}); const calendarEl = document.getElementById("calendar"); const todayKey = today.getFullYear() + "-" + String(today.getMonth() + 1).padStart(2, "0") + "-" + String(today.getDate()).padStart(2, "0"); const todayCell = calendarEl?.querySelector('.day.today[data-date="' + todayKey + '"]'); expect(todayCell).toBeTruthy(); }); it("sets month title from state.lang and given year/month", () => { state.lang = "en"; renderCalendar(2025, 1, {}, {}); const titleEl = document.getElementById("monthTitle"); expect(titleEl?.textContent).toContain("2025"); expect(titleEl?.textContent).toContain("February"); }); });